From f6cc48b8348e524ef1dce935aca41f3508b860b5 Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Sun, 5 Jul 2026 04:48:50 +0000 Subject: [PATCH] fix(#5501): clear pending retry state on render invalidation (Codex gate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex re-gate SILENT: a retry whose fetch is invalidated mid-flight (e.g. a profile switch) left the button stuck as an inert 'Retrying…' with no request in flight — the stale catch returns before _showSessionListLoadError and the .finally() bails when the skeleton removed the old button. _invalidateSessionListRenders() now clears the pending retry markers (retrying, _retryFailedFocus) so the next repaint shows an actionable idle Retry. Regression test added (fails without the fix); test harness extracts _invalidateSessionListRenders + declares its render-gen globals. Co-authored-by: rodboev --- static/sessions.js | 11 +++++ ...issue5501_session_list_retry_affordance.py | 40 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/static/sessions.js b/static/sessions.js index 48d0701b..3fd49248 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -4251,6 +4251,17 @@ function _invalidateSessionListRenders(){ _renderSessionListGen++; _pendingSessionListPayload = null; _renderSessionListQueuedRequest = null; + // A retry whose fetch is invalidated here (e.g. a profile switch mid-retry) + // would otherwise leave the error note stuck as an inert "Retrying…" button + // with no request in flight — the stale fetch returns before + // _showSessionListLoadError and the .finally() bails when the old button was + // removed. Clear the pending retry markers so the next repaint shows an + // actionable idle Retry again. + if(_sessionListLoadError && (_sessionListLoadError.retrying || _sessionListLoadError._retryFailedFocus)){ + _sessionListLoadError = {..._sessionListLoadError}; + delete _sessionListLoadError.retrying; + delete _sessionListLoadError._retryFailedFocus; + } } if(typeof window!=='undefined') window._invalidateSessionListRenders = _invalidateSessionListRenders; diff --git a/tests/test_issue5501_session_list_retry_affordance.py b/tests/test_issue5501_session_list_retry_affordance.py index e5b4c9cb..ea0c0cc8 100644 --- a/tests/test_issue5501_session_list_retry_affordance.py +++ b/tests/test_issue5501_session_list_retry_affordance.py @@ -39,6 +39,7 @@ def _build_node_script(extra_js): src = SESSIONS_JS.read_text(encoding="utf-8") show_error_fn = _extract_function(src, "_showSessionListLoadError") retry_note_fn = _extract_function(src, "_renderSessionListLoadErrorNote") + invalidate_fn = _extract_function(src, "_invalidateSessionListRenders") return f""" class FakeClassList {{ constructor(node) {{ @@ -146,6 +147,9 @@ global.document = {{ createElement: (tag) => new FakeElement(tag) }}; global.$ = (id) => id === 'sessionList' ? list : null; global._allSessions = []; global._sessionListLoadError = null; +global._renderSessionListGen = 0; +global._pendingSessionListPayload = null; +global._renderSessionListQueuedRequest = null; global._sessionListFromCacheCalls = []; global.renderSessionListCalls = []; global.renderSessionListFromCache = () => {{ @@ -184,6 +188,7 @@ global.renderSessionList = (opts) => {{ }}; {show_error_fn} {retry_note_fn} +{invalidate_fn} {extra_js} """ @@ -579,3 +584,38 @@ def test_session_list_error_note_has_live_region_and_pending_uses_aria_disabled( assert "Retrying..." not in note_fn # keyboard focus restored on a failed retry assert "_retryFailedFocus" in note_fn + + +def test_retry_pending_state_cleared_on_render_invalidation(): + """#5505 gate (Codex SILENT): a retry whose fetch is invalidated mid-flight + (e.g. a profile switch) must not leave the button stuck as an inert + 'Retrying…' with no request in flight. _invalidateSessionListRenders() clears + the pending retry markers so the next repaint shows an actionable idle Retry.""" + if NODE is None: + pytest.skip("node not on PATH") + + script = _render_script( + """ +_showSessionListLoadError(new Error('backend busy')); +global._sessionListLoadError = {...global._sessionListLoadError, retrying: true, _retryFailedFocus: true}; +_invalidateSessionListRenders(); +global.renderSessionListFromCache(); +const note = list.children[0]; +const retry = findButton(note); +console.log(JSON.stringify({ + retryingCleared: !global._sessionListLoadError.retrying, + focusFlagCleared: !global._sessionListLoadError._retryFailedFocus, + text: retry.textContent, + ariaDisabled: retry.getAttribute('aria-disabled'), + clickable: typeof retry.onclick === 'function', +})); +""" + ) + body = _run_node(script) + assert body == { + "retryingCleared": True, + "focusFlagCleared": True, + "text": "Retry", + "ariaDisabled": None, + "clickable": True, + }