From fb5a40f1326cb38dbd36d1f6fab717fc26f6d9f8 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Wed, 12 Aug 2026 06:52:45 +0000 Subject: [PATCH] docs(changelog): stamp #6938 + add loadSession compression-cleanup regression test Co-authored-by: happy5318 --- CHANGELOG.md | 2 + ...sue6572_loadsession_compression_cleanup.py | 97 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 tests/test_issue6572_loadsession_compression_cleanup.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a7bea1f..2d66a032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Fixed +- **No more phantom "Compressing context" barrier after switching sessions.** The compression UI state is per-session, but `loadSession()` did not clear it on a switch, so a compression card from a prior session could leak across the load and appear as a phantom "Compressing context" barrier on a fresh session that never triggered compression. Switching sessions now clears the compression UI (state, elapsed timer, composer session-lock, and barrier element) alongside the other per-session resets, before the transcript loads. A compression genuinely running on the session you switch *to* is unaffected — it re-surfaces from that session's own live event stream (and manual compression from its server job status). Thanks @happy5318. (#6938) + - **Layout test helper no longer flags scroll-reachable fields as off-viewport.** The shared `assert_layout_sane` degenerate check reported a false "interactive element is off-viewport" for an input that sits below the fold at rest but is reachable by scrolling an `overflow-y:auto` ancestor (e.g. a tall dialog on a short landscape viewport). It now exempts elements whose off-viewport edge is absorbed by a scrollable ancestor, while still flagging genuinely-unreachable elements (no scroll escape). The Kanban board settings modal layout test also drops its synthetic 480×320 short-landscape case (a viewport no real device uses, whose sub-fold geometry was environment-marginal and CI-flaky); real desktop/tablet/narrow viewports still assert the layout. (internal test infra) ### Changed diff --git a/tests/test_issue6572_loadsession_compression_cleanup.py b/tests/test_issue6572_loadsession_compression_cleanup.py new file mode 100644 index 00000000..92d7fbf3 --- /dev/null +++ b/tests/test_issue6572_loadsession_compression_cleanup.py @@ -0,0 +1,97 @@ +"""Regression: ``loadSession`` must clear stale compression UI state on switch (#6572). + +Context +------- +The compression UI state (``window._compressionUi`` plus its elapsed timer, +composer session-lock, and the "Compressing context" barrier element) is +per-session. Before this fix, ``loadSession`` in ``static/sessions.js`` did not +clear it when switching sessions, so a compression card belonging to a prior +session leaked across the load and surfaced as a phantom "Compressing context" +barrier on a fresh session that never triggered compression. + +The fix clears the state near the top of ``loadSession`` — alongside the other +per-session resets (``_yoloEnabled=false``, ``stopClarifyPolling``, +``hideClarifyCard``) and before transcript loading — by calling the canonical +``clearCompressionUi()`` teardown (defined in ``static/ui.js``), with a +fail-soft ``window._compressionUi=null`` fallback if the function is not yet +defined: + + if(typeof clearCompressionUi==='function') clearCompressionUi(); + else window._compressionUi=null; + +This is safe for a compression genuinely running on the *target* session: +clearing only tears down browser-local UI state; an active automatic +compression re-surfaces from the target session's own SSE ``compressing`` +stream, and a manual compression is re-discovered from server job status when +its session loads. Releasing the composer session-lock does not signal or +cancel the backend compression. + +Per AGENTS.md the WEBUI suite intentionally avoids a node/jsdom dependency, so +this is a source-level grep + brace-balance assertion — the same convention the +rest of the JS regression suite uses (e.g. +``test_bg_task_complete_loadsession_stream_restart.py``). +""" +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +def _read_sessions_js() -> str: + return (REPO_ROOT / "static" / "sessions.js").read_text() + + +def _load_session_body() -> str: + """Return the ``async function loadSession(`` source slice → next top-level + ``function`` / ``async function`` declaration.""" + js = _read_sessions_js() + start = js.index("async function loadSession(") + rest = js[start + 1 :] + m = re.search(r"\n(async function |function )", rest) + end = start + 1 + (m.start() if m else len(rest)) + return js[start:end] + + +def test_loadsession_clears_compression_ui_state(): + """loadSession must clear per-session compression UI state on switch.""" + body = _load_session_body() + # The canonical teardown must be invoked, with a fail-soft fallback so a + # partial script load cannot leave a dangling barrier/timer/lock. + assert "clearCompressionUi()" in body, ( + "loadSession must call clearCompressionUi() to clear stale per-session " + "compression state (#6572 phantom barrier)" + ) + assert re.search( + r"typeof\s+clearCompressionUi\s*===\s*['\"]function['\"]", body + ), "the clearCompressionUi call must be typeof-guarded" + assert re.search( + r"else\s+window\._compressionUi\s*=\s*null", body + ), "a fail-soft window._compressionUi=null fallback must guard the guarded call" + + +def test_loadsession_clears_compression_before_transcript_load(): + """The clear must precede transcript rendering so no phantom barrier shows. + + It must sit with the other early per-session resets (near hideClarifyCard), + not after renderMessages(), otherwise a stale barrier could paint for a + frame on the freshly loaded session. + """ + body = _load_session_body() + clear_idx = body.index("clearCompressionUi") + # Anchored to a sibling early reset that is known to run near the top. + assert "hideClarifyCard" in body, "expected hideClarifyCard reset in loadSession" + clarify_idx = body.index("hideClarifyCard") + # The compression clear should be co-located with the other early resets. + assert abs(clear_idx - clarify_idx) < 600, ( + "clearCompressionUi() should sit with the other early per-session resets " + "(near hideClarifyCard), before transcript loading" + ) + # And it must come before the success-tail render. + render_match = re.search(r"renderMessages\s*\(", body) + if render_match: + assert clear_idx < render_match.start(), ( + "clearCompressionUi() must run before renderMessages() so a stale " + "compression barrier never paints on the loaded session" + )