* fix(sidebar): hoist _sessionAttentionState to top-level scope (#3696) _sessionAttentionState was declared inside renderSessionListFromCache() and relied on function hoisting, but the top-level function _sidebarRowHasVisible Messages (reached via renderSessionListFromCache -> _partitionSidebarSessionRows) called it bare. Hoisting is scoped to the enclosing function, so every sidebar cache-render threw 'ReferenceError: _sessionAttentionState is not defined' and the session list went blank. Regressed in #3672 (v0.51.269) when _sidebarRow HasVisibleMessages was extracted to top level. Fix: move _sessionAttentionState to top-level scope (it is pure — only uses its arg plus the i18n global t), so both the visibility predicate and the nested per-row renderer can reach it. Prevention (the durable half): add scripts/scope_undef_gate.py — models the classic-<script> shared global scope (union of all static files' top-level symbols) and runs ESLint no-undef per file, flagging a function defined nested but called from a sibling scope. Wired into CI (.github/workflows/tests.yml lint job) alongside the existing no-const-assign runtime gate, plus an in-suite test (test_static_js_scope_undef.py) and a focused structural regression test (test_issue3696_session_attention_scope.py). RED/GREEN-validated against the broken tree. * fix(streaming): thread source param into stale-stream bailout; tighten scope gate Opus review of #3698 found the new scope_undef_gate's 'source' allowlist entry was masking a real same-class bug: _bailOutOfTerminalEventsFromStaleStream (declared inside attachLiveStream, params activeSid/streamId/uploaded/options) called _closeSource(source) against a 'source' not in its lexical scope. All 5 call sites are inside _wireSSE(source), but JS scope is lexical not dynamic, so the helper would throw ReferenceError: source is not defined on the stale-stream terminal-event path (user back in an active session whose old stream finalizes late). Fix: thread source as an explicit parameter (declaration + all 5 call sites), the same make-the-dependency-explicit fix as #3696 — and REMOVE the 'source' allowlist entry so the gate stays gated against that name (it now passes because the bug is fixed, not because it's allowlisted). Added the documented false-negative classes from Opus's review to the gate docstring (name-collision shadowing, destructuring-regex gap, exposure escape hatches, name-keyed allowlist) and a focused regression test. This is the prevention gate catching a real latent bug on its first outing. --------- Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
84 lines
3.9 KiB
Python
84 lines
3.9 KiB
Python
"""Regression test for #3696 — `_sessionAttentionState is not defined`.
|
|
|
|
Bug: `_sessionAttentionState` was declared INSIDE `renderSessionListFromCache()`
|
|
and relied on "function hoisting", but the separate top-level function
|
|
`_sidebarRowHasVisibleMessages` (reached via renderSessionListFromCache ->
|
|
_partitionSidebarSessionRows) called it BARE. Function hoisting is scoped to the
|
|
enclosing function, so the call threw `ReferenceError: _sessionAttentionState is
|
|
not defined` on every sidebar cache-render — the session list went blank
|
|
(v0.51.269, regressed by #3672 when _sidebarRowHasVisibleMessages was extracted
|
|
to top level).
|
|
|
|
Fix: hoist `_sessionAttentionState` to top-level (module/global) scope so both the
|
|
top-level visibility predicate and the nested per-row renderer can reach it.
|
|
|
|
This is a structural test (no node/eslint needed, runs in every shard). The
|
|
behavioral scope-analysis guard lives in tests/test_static_js_scope_undef.py +
|
|
scripts/scope_undef_gate.py, which catch the whole class. This test pins the
|
|
specific #3696 invariant cheaply.
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
SESSIONS_JS = (Path(__file__).resolve().parents[1] / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _brace_body(src: str, open_brace_idx: int) -> tuple[int, int]:
|
|
"""Return (start, end) char offsets of the body delimited by the brace at
|
|
open_brace_idx (exclusive of the braces)."""
|
|
depth = 1
|
|
i = open_brace_idx + 1
|
|
while i < len(src) and depth:
|
|
if src[i] == "{":
|
|
depth += 1
|
|
elif src[i] == "}":
|
|
depth -= 1
|
|
i += 1
|
|
return open_brace_idx + 1, i - 1
|
|
|
|
|
|
def _function_span(src: str, name: str) -> tuple[int, int]:
|
|
m = re.search(r"function\s+" + re.escape(name) + r"\s*\(", src)
|
|
assert m, f"function {name} not found"
|
|
brace = src.find("{", m.end())
|
|
return _brace_body(src, brace)
|
|
|
|
|
|
def test_session_attention_state_is_top_level():
|
|
"""`_sessionAttentionState` must be declared at top-level scope (column 0),
|
|
not nested inside another function — otherwise the top-level callers throw
|
|
ReferenceError (#3696)."""
|
|
decls = re.findall(r"^(\s*)function\s+_sessionAttentionState\s*\(", SESSIONS_JS, re.M)
|
|
assert decls, "_sessionAttentionState declaration not found"
|
|
assert len(decls) == 1, f"expected exactly one declaration, found {len(decls)}"
|
|
assert decls[0] == "", (
|
|
"#3696: _sessionAttentionState must be a TOP-LEVEL function (no leading "
|
|
f"indentation), but it is indented {len(decls[0])} spaces (nested). A nested "
|
|
"declaration only hoists within its enclosing function, so top-level callers "
|
|
"like _sidebarRowHasVisibleMessages throw 'ReferenceError: _sessionAttentionState "
|
|
"is not defined'."
|
|
)
|
|
|
|
|
|
def test_session_attention_state_not_nested_in_render_from_cache():
|
|
"""Belt-and-suspenders: the definition must NOT live inside the body of
|
|
`renderSessionListFromCache` (where it was when #3696 shipped)."""
|
|
start, end = _function_span(SESSIONS_JS, "renderSessionListFromCache")
|
|
body = SESSIONS_JS[start:end]
|
|
assert "function _sessionAttentionState(" not in body, (
|
|
"#3696: _sessionAttentionState is defined inside renderSessionListFromCache() — "
|
|
"it must be hoisted to top-level so _sidebarRowHasVisibleMessages can call it."
|
|
)
|
|
|
|
|
|
def test_sidebar_visibility_predicate_calls_attention_state():
|
|
"""Guard the regression's trigger: _sidebarRowHasVisibleMessages (top-level)
|
|
references _sessionAttentionState. If this call is ever removed the bug can't
|
|
recur, but while it exists the function MUST be top-level (asserted above)."""
|
|
start, end = _function_span(SESSIONS_JS, "_sidebarRowHasVisibleMessages")
|
|
body = SESSIONS_JS[start:end]
|
|
assert "_sessionAttentionState(" in body, (
|
|
"_sidebarRowHasVisibleMessages no longer calls _sessionAttentionState — if this "
|
|
"is intentional, update this test; the #3696 invariant assumes this call exists."
|
|
)
|