The rebase onto current master (which carries #5172's window bumps) left the reopen-path test window at 1600, but #5201 grows startSessionStream so the 'else if (_sessionStreamHiddenSid)' reopen assertion now lands at offset ~1741. Widen to 2200 (matching #5201's sibling windows) so the assertion reaches the code. Test-window only; production unchanged.
107 lines
5.1 KiB
Python
107 lines
5.1 KiB
Python
"""Structural tests for #3996 — close idle SSE on hidden tabs to free the
|
|
HTTP/1.1 connection pool (#3992), plus the reopen-on-re-show correctness fix.
|
|
|
|
Two idle persistent SSE connections per window (gateway stream + per-session
|
|
stream) plus the always-present session-events stream meant 2 windows x 3 = 6 =
|
|
the browser's per-origin HTTP/1.1 connection limit, so any subsequent fetch()
|
|
queued behind a saturated pool and timed out. #3996 adds Page Visibility API
|
|
hooks that close those streams while the tab is hidden and reopen them on
|
|
re-show, mirroring the existing ensureSessionEventsSSE() pattern.
|
|
|
|
These are source-grep checks (the hooks live in static JS with no server round
|
|
trip). The key regression guard is that the *per-session* stream actually
|
|
reopens on re-show: stopSessionStream() nulls _sessionStreamSessionId, so the
|
|
reopen path must NOT depend on that variable (it captures the id into a
|
|
dedicated _sessionStreamHiddenSid before closing).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
MESSAGES_JS = (REPO_ROOT / "static" / "messages.js").read_text(encoding="utf-8")
|
|
SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_gateway_sse_has_visibility_hook():
|
|
"""startGatewaySSE installs a visibilitychange hook that closes on hide."""
|
|
assert "_hermesGatewaySSEVisibilityHook" in SESSIONS_JS
|
|
# Closes on hide, and skips opening while hidden to save pool slots.
|
|
assert "stopGatewaySSE()" in SESSIONS_JS
|
|
start_idx = SESSIONS_JS.find("function startGatewaySSE()")
|
|
assert start_idx != -1
|
|
block = SESSIONS_JS[start_idx:start_idx + 1200]
|
|
assert "visibilitychange" in block
|
|
assert "document.hidden" in block
|
|
|
|
|
|
def test_session_stream_has_visibility_hook():
|
|
"""startSessionStream installs a visibilitychange hook."""
|
|
assert "_hermesSessionStreamVisibilityHook" in MESSAGES_JS
|
|
start_idx = MESSAGES_JS.find("function startSessionStream(sid)")
|
|
assert start_idx != -1
|
|
block = MESSAGES_JS[start_idx:start_idx + 2600]
|
|
assert "visibilitychange" in block
|
|
assert "document.hidden" in block
|
|
# Must skip opening a new EventSource while the tab is hidden.
|
|
assert "if (typeof document !== 'undefined' && document.hidden) {" in block
|
|
|
|
|
|
def test_session_stream_reopens_from_dedicated_var_not_nulled_id():
|
|
"""Regression: the per-session SSE must reopen on re-show.
|
|
|
|
stopSessionStream() sets _sessionStreamSessionId = null, so the visibility
|
|
reopen path must capture the id into a dedicated holder BEFORE closing and
|
|
reopen from that — otherwise the stream closes on hide and never comes back.
|
|
"""
|
|
# Dedicated holder declared at module scope.
|
|
assert "_sessionStreamHiddenSid" in MESSAGES_JS, (
|
|
"expected a dedicated holder var for the hidden-tab session id"
|
|
)
|
|
|
|
start_idx = MESSAGES_JS.find("function startSessionStream(sid)")
|
|
block = MESSAGES_JS[start_idx:start_idx + 2200]
|
|
|
|
# On hide: capture the id, then stop.
|
|
assert "_sessionStreamHiddenSid = _sessionStreamSessionId" in block
|
|
# On re-show: reopen from the captured holder, NOT from the nulled id.
|
|
assert "else if (_sessionStreamHiddenSid)" in block
|
|
assert "void startSessionStream(resumeSid)" in block or \
|
|
"startSessionStream(_sessionStreamHiddenSid)" in block
|
|
|
|
# Guard against the buggy form that reopens off the (already-nulled) id.
|
|
assert "else if (_sessionStreamSessionId) {\n void startSessionStream(_sessionStreamSessionId)" not in block, (
|
|
"reopen must not depend on _sessionStreamSessionId — stopSessionStream() nulls it"
|
|
)
|
|
|
|
|
|
def test_stop_session_stream_still_nulls_session_id():
|
|
"""Confirms the precondition the reopen fix defends against."""
|
|
stop_idx = MESSAGES_JS.find("function stopSessionStream()")
|
|
assert stop_idx != -1
|
|
block = MESSAGES_JS[stop_idx:stop_idx + 400]
|
|
assert "_sessionStreamSessionId = null" in block
|
|
|
|
|
|
def test_session_stream_hidden_open_preserves_id_for_reopen():
|
|
"""A session opened while the tab is ALREADY hidden must still reopen on re-show.
|
|
|
|
Codex CORE: startSessionStream(sid) sets _sessionStreamSessionId and then
|
|
returns early when document.hidden. If it doesn't also record the id in the
|
|
dedicated holder, the visibility handler (which reopens only from the holder)
|
|
never reattaches — silently dropping bg_task_complete / server_turn_started
|
|
for a session loaded in a background tab. The hidden-skip path must set
|
|
_sessionStreamHiddenSid = sid before returning.
|
|
"""
|
|
start_idx = MESSAGES_JS.find("function startSessionStream(sid)")
|
|
block = MESSAGES_JS[start_idx:start_idx + 2800]
|
|
# Find the hidden-tab early-return skip path (distinct from the in-hook
|
|
# `if (document.hidden)` branch) and assert it preserves the id.
|
|
hidden_idx = block.find("!== 'undefined' && document.hidden) {")
|
|
assert hidden_idx != -1, "expected a braced hidden-tab skip block (not a bare return)"
|
|
hidden_block = block[hidden_idx:hidden_idx + 160]
|
|
assert "_sessionStreamHiddenSid = sid" in hidden_block, (
|
|
"hidden-tab skip must record the pending session id for reopen on re-show"
|
|
)
|