Closes #2771. v0.51.117 (PR #2766) introduced a top-level function _inflightStateLimits() in static/ui.js that collided with the window._inflightStateLimits config object set in static/boot.js. Because top-level function declarations in classic (non-module) scripts attach to window, boot.js's assignment overwrote the function reference, and every later _inflightStateLimits() call threw TypeError. _compactInflightState() runs on every send(), so no new chat session could be created — v0.51.117 is effectively unusable. Reported by @jahilldev, with multiple users (@isma3iloiso, @theDanielJLewis, @JHVenn) confirming the bug or reverting to v0.51.116. Fix: rename the function to _getInflightStateLimits() — the window-attached config key stays under its original name (unchanged for any downstream code that reads it). Updates all 4 call sites in static/ui.js. Tests: - Update tests/test_inflight_storage_quota.py — the existing test asserted 'function _inflightStateLimits()' in UI_JS as a positive presence check, which certified the bug. Now asserts the renamed function name is present AND the old colliding name is absent AND no stale call sites remain. - Add tests/test_window_function_collision.py — generalized regression that scans every static JS file for top-level function declarations whose name also appears as the target of 'window.X = {...}' or 'window.X = <number>'. This is the exact shape that broke #2715 (_pinnedSessionsLimit in v0.51.106) and #2771. Test fails with a precise diagnostic naming the file and symbol if the bug class returns. Confirmed test FAILS on current master (unfixed) and PASSES on this branch. Verified end-to-end against the live browser before commit: - typeof window._inflightStateLimits === 'object' (config preserved) - typeof window._getInflightStateLimits === 'function' - _getInflightStateLimits() returns the limits object - saveInflightState() persists to localStorage without throwing Full pytest suite: 6308 passed, 6 skipped, 3 xpassed, 8 subtests passed. Opus advisor: SHIP.
88 lines
4.1 KiB
Python
88 lines
4.1 KiB
Python
"""Regression coverage for browser in-flight localStorage quota handling."""
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
CONFIG_PY = (REPO_ROOT / "api" / "config.py").read_text(encoding="utf-8")
|
|
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
|
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _function_body(src: str, name: str) -> str:
|
|
marker = f"function {name}"
|
|
start = src.index(marker)
|
|
brace = src.index("{", start)
|
|
depth = 1
|
|
i = brace + 1
|
|
while depth and i < len(src):
|
|
if src[i] == "{":
|
|
depth += 1
|
|
elif src[i] == "}":
|
|
depth -= 1
|
|
i += 1
|
|
return src[brace + 1 : i - 1]
|
|
|
|
|
|
def test_inflight_state_is_compacted_before_localstorage_write():
|
|
"""Persisted recovery state must stay bounded instead of storing full long sessions."""
|
|
save_body = _function_body(UI_JS, "saveInflightState")
|
|
compact_body = _function_body(UI_JS, "_compactInflightState")
|
|
|
|
assert "const entry={..._compactInflightState(state),updated_at:Date.now()};" in save_body
|
|
assert "const limits=_getInflightStateLimits();" in compact_body
|
|
assert ".slice(-limits.messages)" in compact_body
|
|
assert ".slice(-limits.toolCalls)" in compact_body
|
|
assert "limits.jsonChars" in UI_JS
|
|
|
|
|
|
def test_inflight_state_limits_are_configurable_from_settings():
|
|
"""Recovery snapshots should be bounded by settings, not hardcoded at 3 sessions / 8 messages."""
|
|
assert '\"inflight_state_max_sessions\": 8' in CONFIG_PY
|
|
assert '\"inflight_state_max_messages\": 24' in CONFIG_PY
|
|
assert '\"inflight_state_max_tool_calls\": 48' in CONFIG_PY
|
|
assert '\"inflight_state_max_string_chars\": 60000' in CONFIG_PY
|
|
assert '\"inflight_state_max_json_chars\": 1500000' in CONFIG_PY
|
|
assert '\"inflight_state_max_sessions\": (1, 25)' in CONFIG_PY
|
|
assert '\"inflight_state_max_messages\": (1, 100)' in CONFIG_PY
|
|
assert '\"inflight_state_max_tool_calls\": (1, 200)' in CONFIG_PY
|
|
assert '\"inflight_state_max_string_chars\": (1000, 500000)' in CONFIG_PY
|
|
assert '\"inflight_state_max_json_chars\": (100000, 4000000)' in CONFIG_PY
|
|
assert "window._inflightStateLimits={" in BOOT_JS
|
|
assert "maxSessions:parseInt(s.inflight_state_max_sessions||8,10)||8" in BOOT_JS
|
|
assert "messages:parseInt(s.inflight_state_max_messages||24,10)||24" in BOOT_JS
|
|
# The reader function MUST use a different name than the window-attached
|
|
# config object — top-level `function foo(){}` in non-module scripts
|
|
# attaches to `window`, so a collision causes boot.js to overwrite the
|
|
# function with the config object and every later call throws
|
|
# `_inflightStateLimits is not a function`. See #2771.
|
|
assert "function _getInflightStateLimits()" in UI_JS
|
|
assert "function _inflightStateLimits()" not in UI_JS, (
|
|
"Function name must not collide with window._inflightStateLimits "
|
|
"config object (#2771)."
|
|
)
|
|
assert "window._inflightStateLimits" in UI_JS
|
|
assert "INFLIGHT_STATE_MAX_SESSIONS = 3" not in UI_JS
|
|
assert "INFLIGHT_STATE_MAX_MESSAGES = 8" not in UI_JS
|
|
|
|
|
|
def test_inflight_marker_write_handles_quota_by_dropping_recovery_snapshots():
|
|
"""The tiny active-stream marker must not crash submit when recovery snapshots fill quota."""
|
|
mark_body = _function_body(UI_JS, "markInflight")
|
|
|
|
assert "try{" in mark_body
|
|
assert "localStorage.setItem(INFLIGHT_KEY, payload);" in mark_body
|
|
assert "_isStorageQuotaError(err)" in mark_body
|
|
assert "localStorage.removeItem(INFLIGHT_STATE_KEY);" in mark_body
|
|
assert mark_body.index("localStorage.removeItem(INFLIGHT_STATE_KEY);") < mark_body.rindex(
|
|
"localStorage.setItem(INFLIGHT_KEY, payload);"
|
|
)
|
|
|
|
|
|
def test_save_inflight_state_clears_snapshots_when_quota_retry_fails():
|
|
"""Quota failures should degrade recovery, not preserve a storage-filling blob."""
|
|
save_body = _function_body(UI_JS, "saveInflightState")
|
|
|
|
assert "catch(err)" in save_body
|
|
assert "if(!_isStorageQuotaError(err)) return;" in save_body
|
|
assert "localStorage.removeItem(INFLIGHT_STATE_KEY);" in save_body
|
|
assert "_writeInflightStateMap({[sid]:entry});" in save_body
|