## Release v0.51.266 — Release IH (stage-r16) One agent-authored APPROVED fix + two un-held streaming/SSE fixes. ### Fixed | Issue/PR | Author | Fix | |----------|--------|-----| | #3635 (#3637) | @nesquena-hermes (nesquena APPROVED) | Composer profile chip reads `S.activeProfile` again — a #3331 regression keyed it on the loaded session's profile, so opening a cross-profile session made the chip disagree with the dropdown checkmark and misrepresent where the next message routes. #3331's project/session-op scoping is unaffected. | | #3587 (#3605) | @rodboev | Reasoning persists to the correct intermediate assistant message in multi-turn tool flows. The index only advanced in `on_interim_assistant` (suppressed for contentless tool-call messages) → post-tool reasoning was mis-attributed; it now also advances at the `on_tool` boundary, guarded against over-increment. **(un-held — finding resolved)** | | #2660 (#3558) | @franksong2702 | Session-event SSE no longer wakes every tab across profiles and never drops a relevant refresh — profile attached when known, root/`default` aliases stay unscoped, and the `maxsize=1` queue falls back to unscoped refresh-all on a profile-mismatch coalesce. **(un-held — both findings resolved)** | ### Gate - Full pytest suite: **7770 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): **SAFE TO SHIP** — chip matches dropdown/routing (no #3331 scoping regression), reasoning-index advance composes with the agent's tool/interim callback ordering, session-events coalesce safely with no dropped refresh and no profile data leak (`/api/sessions` still server-side filtered). Co-authored-by: nesquena <nesquena@users.noreply.github.com> Co-authored-by: rodboev <rodboev@users.noreply.github.com> Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
124 lines
5.3 KiB
Python
124 lines
5.3 KiB
Python
"""Regression coverage for manual session title regeneration controls (#3106)."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import api.streaming as streaming
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
|
ROUTES_PY = (ROOT / "api" / "routes.py").read_text(encoding="utf-8")
|
|
STREAMING_PY = (ROOT / "api" / "streaming.py").read_text(encoding="utf-8")
|
|
CHANGELOG = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_session_action_menu_exposes_regenerate_title_control():
|
|
assert "session_title_regenerate" in SESSIONS_JS
|
|
assert "session_title_regenerate_desc" in SESSIONS_JS
|
|
assert "ICONS.spark" in SESSIONS_JS
|
|
assert "api('/api/session/title/regenerate'" in SESSIONS_JS
|
|
assert "renderSessionListFromCache();" in SESSIONS_JS
|
|
|
|
|
|
def test_imported_sessions_skip_regenerate_action_without_broadening_shared_gate():
|
|
# The shared _isReadOnlySession() helper must stay scoped to read_only flags
|
|
# so it does not silently disable rename/pin/archive/etc. for imported
|
|
# sessions. The is_imported guard is scoped to the regenerate action only,
|
|
# matching the backend 403 guard in api/routes.py.
|
|
helper_idx = SESSIONS_JS.index("function _isReadOnlySession(session)")
|
|
next_helper_idx = SESSIONS_JS.index("function _sourceKeyForSession", helper_idx)
|
|
helper_block = SESSIONS_JS[helper_idx:next_helper_idx]
|
|
assert "session.is_imported" not in helper_block, (
|
|
"_isReadOnlySession must not include is_imported — it gates rename/pin/archive too"
|
|
)
|
|
# The regenerate action is gated on !session.is_imported next to its api call.
|
|
regen_idx = SESSIONS_JS.index("api('/api/session/title/regenerate'")
|
|
guard_window = SESSIONS_JS[regen_idx - 600:regen_idx]
|
|
assert "if(!session.is_imported){" in guard_window
|
|
|
|
|
|
def test_regenerate_title_i18n_and_changelog_entries_exist():
|
|
for key in [
|
|
"session_title_regenerate",
|
|
"session_title_regenerate_desc",
|
|
"session_title_regenerating",
|
|
"session_title_regenerated",
|
|
"session_title_regenerate_failed",
|
|
]:
|
|
assert key in I18N_JS
|
|
assert "session action menu can regenerate conversation titles" in CHANGELOG
|
|
assert "#3106" in CHANGELOG
|
|
|
|
|
|
def test_regenerate_endpoint_persists_generated_title_without_reordering_sidebar():
|
|
endpoint_idx = ROUTES_PY.index('"/api/session/title/regenerate"')
|
|
next_endpoint_idx = ROUTES_PY.index('"/api/personality/set"', endpoint_idx)
|
|
block = ROUTES_PY[endpoint_idx:next_endpoint_idx]
|
|
assert "generate_session_title_for_session" in block
|
|
assert "s.llm_title_generated = True" in block
|
|
assert "s.save(touch_updated_at=False)" in block
|
|
assert "_sync_session_title_to_insights(s)" in block
|
|
assert 'publish_session_list_changed("session_title_regenerate", profile=getattr(s, "profile", None))' in block
|
|
assert "Read-only imported sessions cannot be renamed" in block
|
|
|
|
|
|
def test_regenerate_endpoint_syncs_title_to_state_db_when_enabled():
|
|
helper_idx = ROUTES_PY.index("def _sync_session_title_to_insights")
|
|
endpoint_idx = ROUTES_PY.index('"/api/session/title/regenerate"')
|
|
helper_block = ROUTES_PY[helper_idx:endpoint_idx]
|
|
assert 'load_settings().get("sync_to_insights")' in helper_block
|
|
assert "sync_session_usage" in helper_block
|
|
assert "title=session.title" in helper_block
|
|
assert "message_count=len(messages)" in helper_block
|
|
assert "profile=getattr(session, \"profile\", None)" in helper_block
|
|
|
|
|
|
def test_streaming_helper_generates_title_from_persisted_transcript(monkeypatch):
|
|
session = MagicMock()
|
|
session.messages = [
|
|
{"role": "user", "content": "Please fix the stale sidebar title controls"},
|
|
{"role": "assistant", "content": "I will add a regenerate-title action."},
|
|
]
|
|
|
|
class _ProfileEnv:
|
|
def __enter__(self):
|
|
return None
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
import api.profiles as profiles_api
|
|
monkeypatch.setattr(profiles_api, "profile_env_for_background_worker", lambda *args, **kwargs: _ProfileEnv())
|
|
monkeypatch.setattr(
|
|
streaming,
|
|
"_generate_llm_session_title_via_aux",
|
|
lambda user, assistant, agent=None: ("Sidebar title controls", "llm", "raw"),
|
|
)
|
|
|
|
title, status, raw = streaming.generate_session_title_for_session(session)
|
|
assert title == "Sidebar title controls"
|
|
assert status == "llm"
|
|
assert raw == "raw"
|
|
|
|
|
|
def test_streaming_helper_has_local_fallback_when_llm_title_is_empty(monkeypatch):
|
|
session = MagicMock()
|
|
session.messages = [
|
|
{"role": "user", "content": "Can you triage this GitHub issue and PR review?"},
|
|
{"role": "assistant", "content": "Sure."},
|
|
]
|
|
|
|
class _ProfileEnv:
|
|
def __enter__(self):
|
|
return None
|
|
def __exit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
import api.profiles as profiles_api
|
|
monkeypatch.setattr(profiles_api, "profile_env_for_background_worker", lambda *args, **kwargs: _ProfileEnv())
|
|
monkeypatch.setattr(streaming, "_generate_llm_session_title_via_aux", lambda *args, **kwargs: (None, "llm_empty", ""))
|
|
|
|
title, status, _raw = streaming.generate_session_title_for_session(session)
|
|
assert title == "GitHub Issue Triage"
|
|
assert status == "local_summary:llm_empty"
|