diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a224677..20e8132a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,8 @@ ### Fixed +- **Manual "Check for updates" now works even when automatic update checks are turned off.** Previously, disabling automatic update checks in Preferences → System also disabled the manual "check now" button, leaving no way to update from the WebUI without re-enabling the (noisy) automatic banner. The explicit manual force-check now bypasses the disabled auto-check toggle; the automatic path still honors the setting. Thanks @webtecnica. (#6088, #6082) + - **No more spurious "No response from provider" after a tool-limit turn that did answer.** When a turn hit the per-turn tool-iteration limit (`max_iterations_reached`) but the model had already produced a valid final answer, the settlement classifier still appended a "No response from provider" error card after the good reply — a regression of the earlier false-`no_response` fix, because the suppression path admitted only `_soft_partial_terminal_failure` and deliberately excluded `_tool_limit_reached`. The suppression now also covers `_tool_limit_reached`, but *only* when the transcript already holds a final answer; a tool-limit turn with no final answer still fails honestly, and explicit errors / cancellations / interruptions stay outside the suppression. Thanks @coyaSONG. (#6081, #6079) - **New conversations can no longer become unstartable seconds after you create them.** On installs with a saved browser/password-manager credential and more than a few hundred sessions, the manager would autofill the account name into the sidebar conversation filter, silently firing a content search that walked every session through the cache and evicted the brand-new, not-yet-persisted session (its only copy lived in the in-memory cache) — so `/api/session/draft` and `/api/chat/start` then 404'd and the chat could never be started. Two fixes: the search/filter inputs are now `type="search"` with per-manager opt-out attributes and the Settings + onboarding password fields get a real `
` owner (`display:contents`, layout unchanged) so managers stop treating the sidebar filter as a username field; and `_session_is_evictable()` now proves persistence before evicting even a zero-message session, so an unsaved session you're still composing stays resident. Thanks @ai-ag2026. (#6083, #4765) diff --git a/tests/test_updates_6088_manual_check.py b/tests/test_updates_6088_manual_check.py new file mode 100644 index 00000000..c7f991f7 --- /dev/null +++ b/tests/test_updates_6088_manual_check.py @@ -0,0 +1,84 @@ +"""Route-level tests for #6088 / #6082 — a manual force-check must work even +when automatic update checking is disabled.""" +import io +import json +from urllib.parse import urlparse + +import api.routes as routes + + +class _FakeUpdatesHandler: + """Minimal handler for driving routes.handle_post against /api/updates/check.""" + + def __init__(self, body_bytes: bytes): + self.rfile = io.BytesIO(body_bytes) + self.wfile = io.BytesIO() + self.headers = { + "Content-Length": str(len(body_bytes)), + "Content-Type": "application/json", + } + self.command = "POST" + self.path = "/api/updates/check" + self.client_address = ("127.0.0.1", 12345) + self.status = None + + def send_response(self, status): + self.status = status + + def send_header(self, key, value): + self.headers[key] = value + + def end_headers(self): + pass + + +def _run_updates_check(monkeypatch, *, check_for_updates_enabled, body): + """Drive POST /api/updates/check through the real route dispatch, capturing j().""" + cap = {} + + def _j(_handler, obj, *_a, **kw): + cap["ok"] = obj + cap["status"] = kw.get("status", 200) + return True + + def _bad(_handler, msg, code=400, **_kw): + cap["bad"] = (msg, code) + return True + + monkeypatch.setattr(routes, "j", _j) + monkeypatch.setattr(routes, "bad", _bad) + monkeypatch.setattr( + routes, "load_settings", lambda: {"check_for_updates": check_for_updates_enabled} + ) + # Never let the real check run (network / git); prove only whether it's reached. + sentinel = {"reached_real_check": True} + monkeypatch.setattr("api.updates.check_for_updates", lambda **kw: sentinel) + # The visibility guard is not relevant to this route; allow it through. + monkeypatch.setattr(routes, "_guard_request_session_visibility", lambda *a, **k: True) + + handler = _FakeUpdatesHandler(json.dumps(body).encode()) + routes.handle_post(handler, urlparse("/api/updates/check")) + return cap + + +def test_updates_check_disabled_blocks_auto_check(monkeypatch): + """With check_for_updates off, a normal (non-force) POST still short-circuits.""" + cap = _run_updates_check(monkeypatch, check_for_updates_enabled=False, body={}) + assert cap.get("ok") == {"disabled": True}, cap + + +def test_updates_check_disabled_allows_manual_force(monkeypatch): + """#6082/#6088: a manual force-check bypasses the disabled auto-check toggle.""" + cap = _run_updates_check( + monkeypatch, check_for_updates_enabled=False, body={"force": True} + ) + assert cap.get("ok") == {"reached_real_check": True}, ( + "force=true must bypass the disabled short-circuit and run the real check" + ) + assert cap.get("ok") != {"disabled": True} + + +def test_updates_check_enabled_runs_check_without_force(monkeypatch): + """With check_for_updates on, a normal POST runs the real check (no regression).""" + cap = _run_updates_check(monkeypatch, check_for_updates_enabled=True, body={}) + assert cap.get("ok") == {"reached_real_check": True}, cap