After a hidden interval that added persisted messages (post-turn bg-review
writes, sibling-tab writes, the just-finished main turn flushing), switching
the tab back caused refreshActiveSessionIfExternallyUpdated('visible'|'focus')
to hit remoteCount !== localCount and call loadSession(sid, {force:true}),
which synchronously did S.messages = [] before awaiting the metadata +
messages fetches. The visible result was the entire conversation transcript
blanking for the round-trip and reappearing — '对话突然消失,重刷才回来'.
sessions.js itself warns about this 'disappear and reappear' tradeoff at the
top of the remoteCount/remoteLast guard, but only short-circuits the
metadata-only branch (#5061). #5122's 4-probe staged reconnect covers a
different path (SSE error with ready_state=2 on a visible tab) and does not
apply when the SSE error arrives while visibility_state='hidden' — that
bottoms out through _deferStreamErrorIfPageHidden before the reconnect block
ever runs.
Fix:
- refreshActiveSessionIfExternallyUpdated maps the visibility/focus recovery
reasons ({visible, focus}) to a new keepStaleUntilLoaded option, forwarded
to loadSession. The post-stream idle reconcile and the poll/external paths
keep their existing behaviour.
- loadSession ANDs opts.keepStaleUntilLoaded with sameSessionForceReload (so
cross-session switches still clear synchronously — leaving a prior
session's transcript on screen during a navigation is the original bug
the clear was written for) and, on that path, skips the synchronous
S.messages/S.toolCalls/_messagesTruncated/_oldestIdx clear. The new
transcript is SWAPPED into S.messages by _ensureMessagesLoaded(sid,
{force:true}), so the user sees old DOM directly replaced by new DOM in a
single render frame.
- _ensureMessagesLoaded grows an opts.force escape hatch so its 'messages
already populated' early-return cannot skip the swap when stale messages
are still in place.
Verified live on a source 8701 build:
- before: minHtmlObserved 65874, minKidsObserved 11 — 372 samples at 16ms,
DOM html length NEVER dipped below the pre-call value during the reload.
- loadSession was called with force:true keepStale:true reason:'visible',
outcome 'reloaded', sMsgs 21 → 1037, after kids 11 → 134 (single-frame
swap).
Tests:
- tests/test_issue5177_hidden_tab_blank_gap.py (new, 7 cases): source-locks
the keep-stale guard shape, the recovery-reason map, the
_ensureMessagesLoaded opts.force escape hatch, and that the synchronous
clear is wrapped in if(!_keepStaleUntilLoaded).
- Updated the two existing source-lock tests whose anchor strings included
the loadSession call signature
(tests/test_webui_external_refresh_frontend.py +
tests/test_tars_scroll_reset_regressions.py) to match the new
loadSession(... keepStaleUntilLoaded:_keepStaleUntilLoaded) form. No
behavioural assertions changed.
Local: node --check static/sessions.js OK. 122 passed across
test_issue5177_hidden_tab_blank_gap.py + the full adjacent set
(external-refresh / journal-frontend / scroll-reset / reconnect-chronology /
inflight-stream-reuse / sse-error-multi-probe / issue4295 / issue4856).
44 lines
2.0 KiB
Python
44 lines
2.0 KiB
Python
"""Regression test pinning the #3162 fix.
|
|
|
|
#3162: `_ensureMessagesLoaded` in static/sessions.js declared `const msgs` but the
|
|
#3018 ephemeral-field carry-forward reassigns it (`msgs = window._carryForwardEphemeralTurnFields(...)`).
|
|
`const` -> runtime TypeError -> "Failed to load conversation messages" toast on every
|
|
mobile message (v0.51.161-166). Fix: `const` -> `let`.
|
|
|
|
This is the targeted pin; tests/test_static_js_runtime_lint.py is the general guard.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
SESSIONS_JS = (REPO / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _ensure_messages_loaded_body() -> str:
|
|
start = SESSIONS_JS.index("async function _ensureMessagesLoaded")
|
|
# Window widened (#3326 added reload-width-hint handling inside this function,
|
|
# pushing the carry-forward reassignment further down; #3790 added the
|
|
# cold-load expand_renderable param + comment, pushing it further still).
|
|
return SESSIONS_JS[start: start + 4500]
|
|
|
|
|
|
def test_ensure_messages_loaded_declares_msgs_with_let():
|
|
body = _ensure_messages_loaded_body()
|
|
assert "let msgs = (data.session.messages" in body, (
|
|
"_ensureMessagesLoaded must declare `let msgs` — it is reassigned by the #3018 "
|
|
"carry-forward, and `const` throws a runtime TypeError on every mobile message (#3162)"
|
|
)
|
|
assert "const msgs = (data.session.messages" not in body, (
|
|
"found `const msgs` in _ensureMessagesLoaded — this is the #3162 brick-class bug; "
|
|
"must be `let` because msgs is reassigned"
|
|
)
|
|
|
|
|
|
def test_ensure_messages_loaded_reassignment_still_present():
|
|
"""Keep this test meaningful: confirm the reassignment that requires `let` exists.
|
|
If the carry-forward is removed, revisit whether `let` is still needed."""
|
|
body = _ensure_messages_loaded_body().replace(" ", "")
|
|
assert "msgs=window._carryForwardEphemeralTurnFields" in body, (
|
|
"the #3018 carry-forward reassignment of msgs is gone — re-evaluate the let/const "
|
|
"decision in _ensureMessagesLoaded"
|
|
)
|