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:
nesquena-hermes committed 2026-06-18 22:21:03 +00:00
1 parent b26c184bf5
commit 7c21fe4a93
26 files changed
+67 -31

No files matched your search

+6
View File
@@ -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
+18
View File
@@ -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
+12
View File
@@ -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:
+1 -1
View File
@@ -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}")
+1 -1
View File
@@ -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}")
+2 -2
View File
@@ -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}")
+1 -1
View File
@@ -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}")
+1 -1
View File
@@ -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}")
+1 -1
View File
@@ -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}")
+1 -1
View File
@@ -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}")
+2 -2
View File
@@ -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}")
+1 -1
View File
@@ -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}")
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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}")
+1 -1
View File
@@ -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}")
+1 -1
View File
@@ -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}")
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)