Files
hermes-webui/tests/test_issue3660_context_window_stale.py
nesquena-hermes 8ef698ea05
Some checks failed
Release & Docker / release (push) Has been cancelled
Release v0.51.277 — Release IS (stage-p3f — preserve context-window in usage indicator #3663) (#3683)
* fix(ui): preserve resolved context window in usage indicator (#3663, #3185, #3660)

Co-authored-by: Frank Song <franksong2702@gmail.com>

* docs(changelog): v0.51.277 — Release IS (stage-p3f)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-06-05 13:45:15 -07:00

65 lines
3.2 KiB
Python

"""Regression coverage for #3660/#3185 stale context-window indicators.
The live SSE metering path can know the real model context window while the
terminal `done` snapshot or cold session snapshot omits that field. The UI must
not replace a known 1M window with the JavaScript 128K fallback on settlement or
history reload.
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MESSAGES_JS = (ROOT / "static" / "messages.js").read_text(encoding="utf-8")
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
def _listener_block(event_name: str, next_event_name: str) -> str:
start = MESSAGES_JS.find(f"source.addEventListener('{event_name}'")
assert start != -1, f"{event_name} listener not found"
end = MESSAGES_JS.find(f"source.addEventListener('{next_event_name}'", start)
assert end != -1, f"{next_event_name} listener after {event_name} not found"
return MESSAGES_JS[start:end]
def test_done_event_preserves_last_known_context_window():
block = _listener_block("done", "stream_end")
assert "S.lastUsage=d.usage;_syncCtxIndicator(d.usage);" not in block
assert "const _doneUsageFallback={...(S.lastUsage||{})};" in block
assert "_doneUsageFallback[_usageField]=S.session[_usageField];" in block
assert "_mergeUsageForCtxIndicator(d.usage,_doneUsageFallback)" in block
assert "_syncCtxIndicator(S.lastUsage);" in block
def test_usage_merge_helper_keeps_context_when_latest_omits_it():
start = UI_JS.find("function _mergeUsageForCtxIndicator")
assert start != -1, "context usage merge helper not found"
end = UI_JS.find("// Context usage indicator in composer footer", start)
assert end != -1, "context usage merge helper end marker not found"
block = UI_JS[start:end]
assert "const merged={...latestObj};" in block
assert "'input_tokens','output_tokens','estimated_cost'," in block
assert "if(!(Number(latestObj.context_length)>0)&&Number(fallbackObj.context_length)>0)" in block
assert "merged.context_length=fallbackObj.context_length;" in block
assert "threshold_tokens" in block
assert "last_prompt_tokens" in block
def test_session_load_prefers_positive_last_usage_context_over_stale_snapshot():
assert "const _pickPositive=(latest,stored,dflt=0)=>Number(latest)>0?latest:(Number(stored)>0?stored:dflt);" in SESSIONS_JS
assert "context_length: _pickPositive(u.context_length, _s.context_length)," in SESSIONS_JS
def test_deferred_model_resolve_does_not_zero_out_existing_context_window():
block_start = SESSIONS_JS.find("function _resolveSessionModelForDisplaySoon")
assert block_start != -1, "deferred model resolver not found"
block_end = SESSIONS_JS.find("// Tracks whether the current session has older messages", block_start)
assert block_end != -1, "deferred model resolver end marker not found"
block = SESSIONS_JS[block_start:block_end]
assert "const resolvedContextLength=data.session.context_length||S.session.context_length||0;" in block
assert "S.session.context_length=resolvedContextLength;" in block
assert "context_length:resolvedContextLength||u.context_length||0," in block