Files
hermes-webui/tests/test_model_selection_refresh_persistence.py
T
65c4bc9fa2 Release v0.51.295 — stage-3739/3742 (model-pick revert fix #3739 + session-status revert #3742) (#3743)
* fix: honor explicit model pick, suppress silent revert on cross-family selection (#3737)

When a user changes the model in the composer dropdown and sends,
_resolve_compatible_session_model_state previously had no way to
distinguish an explicit user pick from stale session state. The
profile-aware branch (v0.51.290, PR #3448) and the legacy block
both rewrote bare cross-family models to the profile default, and
the client unconditionally applied effective_model — silently
discarding the user's choice.

Backend: accept explicit_model_pick flag (default False) on
_resolve_compatible_session_model_state. Guard both the
profile-aware branch (routes.py:2024) and the legacy block
(routes.py:2124) to skip cross-provider normalization when set.
_handle_chat_start extracts the flag and passes it through.

Frontend: consult _readPendingSessionModel (sessionStorage, 10-min
window) to detect explicit picks and include the flag. Add a toast
as defense-in-depth when the server still returns effective_model.

Closes #3737

* fix: tighten explicit-pick detection and add regression tests (#3737)

Greptile P2-1: compare model_provider in pending pick detection,
not just model name, to avoid false-positive flag when the
session provider changes between pick and send.

Greptile P2-2: only show the defense-in-depth toast when an
explicit pick was actually overridden — stale-session
normalizations are expected behavior and should be silent.

Add two regression tests for the profile-branch guard:
- explicit_model_pick=True → cross-family model survives
- explicit_model_pick=False → existing normalization preserved

* revert(sidebar): remove manual session status labels (#3570)

The manual per-session status labels (Todo / In Progress / Done) added in
v0.51.284 (#3570) stored state only in browser localStorage keyed by session
id, with no server-side backing — so labels did not persist across browsers
or devices (a user who labeled sessions on one machine saw none after moving
to a laptop). They also rendered as three flat top-level entries in the
session context menu, crowding the root menu.

Per maintainer decision, remove the feature entirely for now. It can be
reintroduced later with proper server-side persistence and a less intrusive
menu treatment.

Removes:
- JS state/cycle helpers + SESSION_MANUAL_STATUS_KEY (static/sessions.js)
- context-menu status entries + sidebar status badge render
- .session-manual-status* CSS (static/style.css)
- session_status_* locale strings across all locales (static/i18n.js)

Full suite: 8084 passed, 0 failed. ESLint runtime gate: clean.

reverts #3570

* fix(#3737): keep explicit-pick marker until send consumes it (Codex catch)

Codex found the explicit_model_pick flag never engaged in the normal flow: boot.js
modelSelect.onchange cleared the pending-pick marker right after /api/session/update,
so by the time send() ran _readPendingSessionModel returned null, _explicitPick was
false, and the server's profile-provider branch still reverted the cross-family pick
(the exact #3737 bug). The flag only worked in the rare race where send beat the
session-update round-trip.

Fix (Codex prescription): do NOT clear the marker in onchange; clear it in send()
immediately after reading a matching pending pick, so it's consumed for that send only.
onchange still RECORDS the pick (_rememberPendingSessionModel) — only the premature
clear is removed.

* test(#3737): lock client clear-timing wiring (onchange records, send consumes)

Static source guards for the Codex clear-timing fix: onchange must record the
pending pick and NOT clear it post-session-update; send() must consume (clear) it
only after reading a matching _explicitPick, and send the flag only when truthy.
Complements the author's resolver-level tests in test_provider_mismatch.py.

* test(#3737): realign refresh-persistence test to the moved pending-pick clear

The Codex clear-timing fix moved the pending-pick clear out of modelSelect.onchange
into send() (consume-on-send). test_model_selection_records_pending_state_before_async_session_update
asserted the OLD onchange-clears behavior (assert _clearPendingSessionModel in body).
Updated to assert the NEW correct behavior (onchange must NOT clear it — it survives to
send). The test's core refresh-survives invariant (marker recorded before the async
session-update; reapplied on load) is unchanged and still passes; only the stale
clear-location assertion is flipped. Not a regression-blessing: the refresh-survives
feature is intact, the marker lifecycle is more correct.

---------

Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 13:39:14 -07:00

61 lines
2.6 KiB
Python

"""Regression coverage for model selection surviving hard refresh.
The frontend updates the visible model chip before the async
``/api/session/update`` request returns. A hard refresh can abort that request,
so the browser must remember the session-scoped selection and reapply it on the
next ``loadSession()`` before ``syncTopbar()`` projects server metadata.
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BOOT_JS = (ROOT / "static" / "boot.js").read_text()
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text()
UI_JS = (ROOT / "static" / "ui.js").read_text()
def _body_between(src: str, start: str, end: str) -> str:
start_idx = src.index(start)
end_idx = src.index(end, start_idx)
return src[start_idx:end_idx]
def test_model_selection_records_pending_state_before_async_session_update():
"""A refresh during /api/session/update must not lose the selected model."""
body = _body_between(BOOT_JS, "$('modelSelect').onchange=async()=>", "$('msg').addEventListener")
pending_idx = body.index("_rememberPendingSessionModel")
local_model_idx = body.index("S.session.model=modelState.model")
update_idx = body.index("await api('/api/session/update'")
assert pending_idx < update_idx
assert local_model_idx < update_idx
# onchange must NOT clear the pending marker after the session-update round-trip.
# The marker has to survive until the next send() consumes it, otherwise the normal
# pick→update→send flow loses the explicit-pick signal and the server re-reverts a
# cross-family pick (#3737). The consume-clear now lives in send(), not here.
assert "_clearPendingSessionModel" not in body, (
"modelSelect.onchange must not clear the pending explicit-pick marker (#3737); "
"send() consumes it instead"
)
def test_load_session_applies_pending_model_before_first_topbar_sync():
"""Reload should project the pending selection before server old metadata wins."""
body = _body_between(SESSIONS_JS, "async function loadSession", "const activeStreamId=")
apply_idx = body.index("_applyPendingSessionModelForSession")
sync_idx = body.index("syncTopbar()")
assert apply_idx < sync_idx
def test_pending_model_helpers_are_session_scoped_and_expire():
assert "const PENDING_SESSION_MODEL_PREFIX" in UI_JS
assert "function _pendingSessionModelKey" in UI_JS
assert "function _rememberPendingSessionModel" in UI_JS
assert "function _applyPendingSessionModelForSession" in UI_JS
assert "propagateErrors:true" in UI_JS
assert "sessionStorage" in UI_JS
assert "10*60*1000" in UI_JS