fix(sessions): invalidate session-list cache on CLI-visibility toggle + de-flake node test timeouts
ROOT FIX (product bug): POST /api/settings never invalidated the session-list cache, so toggling show_cli_sessions/show_cron_sessions could serve a stale sidebar for up to the cache TTL when the settings-file mtime stamp collided with a cached entry. The handler now clears the cache directly on those toggles. This is also the root cause of the recurring test_gateway_sync CI flake (session absent from /api/sessions right after the visibility toggle). DEFENSE-IN-DEPTH: cleanup_test_sessions fixture now resets show_cli_sessions to default BEFORE each test too, not only in teardown. FLAKE FIX (test infra): bump 31 node-subprocess correctness-harness timeouts from 10s -> 30s across 23 test files. Node spawn + V8 warmup under parallel CI shard load can exceed 10s, causing TimeoutExpired flakes (e.g. test_1325 on #4445). Only real node subprocess.run() sites changed — mock signatures, HTTP urlopen, and deliberate timeout-behavior tests (test_api_timeout, timeout=0/1/2) untouched.
This commit is contained in:
1 parent
b26c184bf5
commit
7c21fe4a93
26 files changed
+67
-31
No files matched your search
@@ -3,6 +3,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.502] — 2026-06-18 — Release RL (CLI-session visibility toggle invalidates the sidebar cache)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Toggling "show CLI/messaging sessions" now takes effect immediately instead of after a brief delay.** `POST /api/settings` did not invalidate the session-list cache, which is keyed partly on the visibility setting and otherwise only revalidated by the settings-file mtime stamp. When a visibility toggle wrote the settings file within the same mtime granularity as a cached entry (and resolved to the default-valued cache key), the sidebar could keep serving the stale session set for up to the cache TTL (~2.5s). The settings handler now clears the session-list cache directly whenever `show_cli_sessions` / `show_cron_sessions` change. This was also the root cause of an intermittent `test_gateway_sync` CI flake (a freshly-inserted CLI/gateway session occasionally absent from `/api/sessions` right after the visibility toggle).
|
||||
|
||||
## [v0.51.501] — 2026-06-18 — Release RK (Ctrl/Cmd+, opens Settings)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -9941,6 +9941,24 @@ def handle_post(handler, parsed) -> bool:
|
||||
saved = save_settings(body)
|
||||
saved.pop("password_hash", None) # never expose hash to client
|
||||
|
||||
# Settings that change which sessions appear in the sidebar must
|
||||
# invalidate the session-list cache directly. Relying on the cache's
|
||||
# settings-file mtime stamp is fragile: a toggle that writes the
|
||||
# settings file within the same mtime granularity as a cached entry (and
|
||||
# produces the default-valued key, e.g. show_cli_sessions back to its
|
||||
# True default) can leave a stale row set served for up to the cache TTL.
|
||||
# This is the root cause of the intermittent gateway_sync test flake
|
||||
# (a freshly-inserted CLI/gateway session occasionally absent from
|
||||
# /api/sessions right after the visibility toggle). Invalidate explicitly.
|
||||
if any(
|
||||
k in body
|
||||
for k in ("show_cli_sessions", "show_cron_sessions")
|
||||
):
|
||||
try:
|
||||
_clear_session_list_cache()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
auth_enabled_after = is_auth_enabled()
|
||||
auth_just_enabled = bool(
|
||||
requested_password and auth_enabled_after and not auth_enabled_before
|
||||
|
||||
@@ -1073,6 +1073,18 @@ def cleanup_test_sessions():
|
||||
was left in an unexpected state by a prior test in the same shard.
|
||||
"""
|
||||
created: list[str] = []
|
||||
# Defense-in-depth: reset the CLI-session visibility setting to its default
|
||||
# BEFORE the test runs too, not only in teardown. Teardown-only reset relies
|
||||
# on every sibling test being wrapped by this fixture AND on its teardown
|
||||
# actually completing; a pre-test reset guarantees each test starts from a
|
||||
# known visibility state regardless of what a prior test left behind, so a
|
||||
# test that reads /api/sessions without first setting the toggle can't
|
||||
# inherit a leaked value. Pairs with the POST /api/settings cache
|
||||
# invalidation that fixes the gateway_sync row-absence flake at the source.
|
||||
try:
|
||||
_post(TEST_BASE, "/api/settings", {"show_cli_sessions": False})
|
||||
except Exception:
|
||||
pass
|
||||
yield created
|
||||
|
||||
for sid in created:
|
||||
|
||||
@@ -46,7 +46,7 @@ def _run_user_render(text_input):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['node', tf.name, json.dumps(text_input)],
|
||||
capture_output=True, text=True, timeout=10
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node error: {result.stderr}")
|
||||
|
||||
@@ -278,7 +278,7 @@ setTimeout_(() => {
|
||||
)
|
||||
r = subprocess.run(
|
||||
[NODE, "-e", js],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"node failed: {r.stderr}")
|
||||
|
||||
@@ -126,7 +126,7 @@ def _run_helper(driver_path, payload):
|
||||
[NODE, driver_path, str(UI_JS_PATH), json.dumps(payload)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed:\nSTDOUT={result.stdout}\nSTDERR={result.stderr}")
|
||||
@@ -139,7 +139,7 @@ def _run_model_state_helper(driver_path, payload):
|
||||
[NODE, driver_path, str(UI_JS_PATH), json.dumps(payload)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed:\nSTDOUT={result.stdout}\nSTDERR={result.stderr}")
|
||||
|
||||
@@ -64,7 +64,7 @@ def _render(driver_path, markdown: str) -> str:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
[NODE, driver_path, str(UI_JS_PATH)],
|
||||
input=markdown, capture_output=True, text=True, timeout=10,
|
||||
input=markdown, capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
|
||||
@@ -77,7 +77,7 @@ def _find(driver_path, model_id: str, options: list[str], preferred: str | None
|
||||
result = subprocess.run(
|
||||
[NODE, driver_path, str(UI_JS_PATH),
|
||||
json.dumps({"modelId": model_id, "options": options, "preferredProvider": preferred})],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
|
||||
@@ -233,7 +233,7 @@ def _render(driver_path: str, markdown: str) -> str:
|
||||
input=markdown,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
|
||||
@@ -169,7 +169,7 @@ def _render(driver_path, markdown: str) -> str:
|
||||
input=markdown,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
|
||||
@@ -164,7 +164,7 @@ def _run_sync(driver_path, *, session_model, initial_value="@expensive:gpt-5.5",
|
||||
[NODE, driver_path, str(UI_JS_PATH), json.dumps(payload)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed:\nSTDOUT={result.stdout}\nSTDERR={result.stderr}")
|
||||
|
||||
@@ -63,7 +63,7 @@ def _render(driver_path: str, markdown: str) -> str:
|
||||
input=markdown,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr)
|
||||
|
||||
@@ -114,7 +114,7 @@ def _find(driver_path, model_id, options, preferred=None):
|
||||
result = subprocess.run(
|
||||
[NODE, driver_path, str(UI_JS_PATH),
|
||||
json.dumps({"modelId": model_id, "options": options, "preferredProvider": preferred})],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
@@ -124,7 +124,7 @@ def _find(driver_path, model_id, options, preferred=None):
|
||||
def _norm_keys(driver_path, ids):
|
||||
result = subprocess.run(
|
||||
[NODE, driver_path, str(UI_JS_PATH), json.dumps(ids)],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
|
||||
@@ -164,7 +164,7 @@ def _resolve(driver, query, groups, sel_options):
|
||||
r = subprocess.run(
|
||||
[NODE, driver, str(COMMANDS_JS_PATH),
|
||||
json.dumps({"query": query, "groups": groups, "selOptions": sel_options})],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {r.stderr}")
|
||||
@@ -175,7 +175,7 @@ def _find(driver, model_id, options, preferred=None):
|
||||
r = subprocess.run(
|
||||
[NODE, driver, str(UI_JS_PATH),
|
||||
json.dumps({"modelId": model_id, "options": options, "preferredProvider": preferred})],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {r.stderr}")
|
||||
@@ -185,7 +185,7 @@ def _find(driver, model_id, options, preferred=None):
|
||||
def _best(driver, query, options):
|
||||
r = subprocess.run(
|
||||
[NODE, driver, str(COMMANDS_JS_PATH), json.dumps({"query": query, "options": options})],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {r.stderr}")
|
||||
|
||||
@@ -92,7 +92,7 @@ def norm_driver(tmp_path_factory):
|
||||
def _labels(driver_path, ids):
|
||||
result = subprocess.run(
|
||||
[NODE, driver_path, str(UI_JS_PATH), json.dumps(ids)],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
@@ -102,7 +102,7 @@ def _labels(driver_path, ids):
|
||||
def _norm_keys(driver_path, ids):
|
||||
result = subprocess.run(
|
||||
[NODE, driver_path, str(UI_JS_PATH), json.dumps(ids)],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
|
||||
@@ -51,7 +51,7 @@ def _run_node(script: str) -> dict:
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
@@ -1394,7 +1394,7 @@ def _run_dropdown_driver(driver_path: str, payload: dict | None = None) -> dict:
|
||||
[NODE, driver_path, str(REPO / "static" / "ui.js"), json.dumps(payload)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed:\nSTDOUT={result.stdout}\nSTDERR={result.stderr}")
|
||||
@@ -1562,7 +1562,7 @@ def test_runtime_inplace_expand_then_search_clear_preserves_expanded_group(_driv
|
||||
[NODE, _driver_paths["inplace"], str(REPO / "static" / "ui.js"), json.dumps(payload)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node inplace driver failed:\nSTDOUT={result.stdout}\nSTDERR={result.stderr}")
|
||||
@@ -1621,7 +1621,7 @@ def test_runtime_inplace_endpoint_error_group_renders_open(_driver_paths):
|
||||
[NODE, _driver_paths["endpoint_error"], str(REPO / "static" / "ui.js"), json.dumps(payload)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node endpoint_error driver failed:\nSTDOUT={result.stdout}\nSTDERR={result.stderr}")
|
||||
@@ -1663,7 +1663,7 @@ def test_runtime_inplace_expand_with_preexisting_options_reveals_them(_driver_pa
|
||||
[NODE, _driver_paths["preexisting"], str(REPO / "static" / "ui.js"), json.dumps(payload)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node preexisting driver failed:\nSTDOUT={result.stdout}\nSTDERR={result.stderr}")
|
||||
|
||||
@@ -138,7 +138,7 @@ def test_backend_frontend_parity_complex_aggregator_proxy():
|
||||
tmp = f.name
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["node", tmp], capture_output=True, text=True, timeout=10
|
||||
["node", tmp], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
assert result.returncode == 0, f"JS execution failed: {result.stderr}"
|
||||
js_pairs = json.loads(result.stdout.strip())
|
||||
|
||||
@@ -26,7 +26,7 @@ def _run_node(source: str) -> str:
|
||||
cwd=str(REPO_ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
finally:
|
||||
script_path.unlink(missing_ok=True)
|
||||
|
||||
@@ -26,7 +26,7 @@ def _run_node(source: str) -> str:
|
||||
cwd=str(REPO_ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
finally:
|
||||
script_path.unlink(missing_ok=True)
|
||||
|
||||
@@ -307,7 +307,7 @@ def _run_populate_driver(
|
||||
[NODE, driver_path, str(REPO / "static" / "ui.js"), json.dumps(payload)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed:\nSTDOUT={result.stdout}\nSTDERR={result.stderr}")
|
||||
|
||||
@@ -108,7 +108,7 @@ def _apply(driver_path, value):
|
||||
import json as _json
|
||||
result = subprocess.run(
|
||||
[NODE, driver_path, str(UI_JS_PATH), _json.dumps(value)],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
|
||||
@@ -80,7 +80,7 @@ def _render(driver_path, markdown: str) -> str:
|
||||
input=markdown,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"node driver failed: {result.stderr}")
|
||||
|
||||
@@ -53,7 +53,7 @@ def _run_session_search_helper(cases):
|
||||
[NODE, "-e", driver, json.dumps(cases)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
@@ -24,7 +24,7 @@ def _run_node(source: str) -> str:
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr)
|
||||
|
||||
@@ -62,7 +62,7 @@ def _preview(driver_path: str, tc: dict, display_snippet: str = "") -> str:
|
||||
input=json.dumps({"tc": tc, "displaySnippet": display_snippet}),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr)
|
||||
|
||||
Reference in new issue
Block a user