225 lines
8.1 KiB
Python
225 lines
8.1 KiB
Python
"""Regression coverage for imported-session title generation after CLI import (#3987)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
from collections import OrderedDict
|
|
from pathlib import Path
|
|
|
|
import api.models as models
|
|
import api.routes as routes
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ROUTES_PY = (ROOT / "api" / "routes.py").read_text(encoding="utf-8")
|
|
SESSIONS_JS = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
|
class _FakeHandler:
|
|
def __init__(self):
|
|
self.status = None
|
|
self.headers = {}
|
|
self.wfile = io.BytesIO()
|
|
|
|
def send_response(self, status):
|
|
self.status = status
|
|
|
|
def send_header(self, key, value):
|
|
self.headers[key] = value
|
|
|
|
def end_headers(self):
|
|
pass
|
|
|
|
def json_body(self):
|
|
return json.loads(self.wfile.getvalue().decode("utf-8"))
|
|
|
|
|
|
def test_import_cli_handler_queues_default_titles_after_persisting_import():
|
|
handler_idx = ROUTES_PY.index("def _handle_session_import_cli")
|
|
next_handler_idx = ROUTES_PY.index("def _handle_session_import(", handler_idx)
|
|
block = ROUTES_PY[handler_idx:next_handler_idx]
|
|
queue_idx = block.index("_queue_generated_title_for_imported_session(")
|
|
publish_idx = block.index('publish_session_list_changed(\n "session_import_cli",')
|
|
response_idx = block.index("return j(", queue_idx)
|
|
assert publish_idx < queue_idx < response_idx
|
|
queue_window = block[queue_idx:queue_idx + 400]
|
|
assert '"title": cli_title' in queue_window
|
|
assert '"read_only": cli_read_only' in queue_window
|
|
|
|
|
|
def test_import_cli_queue_helper_is_guarded_and_runs_in_background():
|
|
helper_idx = ROUTES_PY.index("def _queue_generated_title_for_imported_session")
|
|
next_helper_idx = ROUTES_PY.index("def _gateway_sse_probe_payload", helper_idx)
|
|
block = ROUTES_PY[helper_idx:next_helper_idx]
|
|
assert "cli_meta.get(\"read_only\")" in block
|
|
assert "not _looks_like_default_cli_title(cli_meta)" in block
|
|
assert "if not _looks_like_default_cli_title(current_meta):" in block
|
|
assert "generate_session_title_for_session(current)" in block
|
|
assert "require_default_title=True" in block
|
|
assert "threading.Thread(target=_run, daemon=True" in block
|
|
|
|
|
|
def test_import_cli_queue_helper_generates_title_once_for_placeholder_session(monkeypatch):
|
|
persisted = []
|
|
generated = []
|
|
|
|
class FakeSession:
|
|
def __init__(self, title):
|
|
self.session_id = "cli_queued_title"
|
|
self.title = title
|
|
self.source_tag = "cli"
|
|
self.raw_source = "cli"
|
|
self.session_source = "external_agent"
|
|
self.source_label = "CLI"
|
|
self.read_only = False
|
|
|
|
current = FakeSession("CLI Session")
|
|
|
|
class InlineThread:
|
|
def __init__(self, *, target, daemon, name):
|
|
self._target = target
|
|
|
|
def start(self):
|
|
self._target()
|
|
|
|
monkeypatch.setattr(routes.threading, "Thread", InlineThread)
|
|
monkeypatch.setattr(routes.Session, "load", classmethod(lambda _cls, sid: current if sid == current.session_id else None))
|
|
monkeypatch.setattr(routes, "_ensure_full_session_before_mutation", lambda sid, session: session)
|
|
monkeypatch.setattr(routes, "generate_session_title_for_session", lambda session: (generated.append(session.session_id) or "Better imported title", "llm", "raw"))
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"_persist_generated_session_title",
|
|
lambda session, title, *, event_reason, require_default_title=False: persisted.append(
|
|
(session.session_id, title, event_reason, require_default_title)
|
|
),
|
|
)
|
|
|
|
routes._queue_generated_title_for_imported_session(current, {"title": "CLI Session", "source_tag": "cli"})
|
|
|
|
assert generated == [current.session_id]
|
|
assert persisted == [(current.session_id, "Better imported title", "session_title_regenerate", True)]
|
|
|
|
|
|
def test_import_cli_queue_helper_skips_sessions_that_already_have_real_titles(monkeypatch):
|
|
generated = []
|
|
persisted = []
|
|
|
|
class FakeSession:
|
|
def __init__(self):
|
|
self.session_id = "cli_real_title"
|
|
self.title = "Useful imported title"
|
|
self.source_tag = "cli"
|
|
self.raw_source = "cli"
|
|
self.session_source = "external_agent"
|
|
self.source_label = "CLI"
|
|
self.read_only = False
|
|
|
|
current = FakeSession()
|
|
|
|
class InlineThread:
|
|
def __init__(self, *, target, daemon, name):
|
|
self._target = target
|
|
|
|
def start(self):
|
|
self._target()
|
|
|
|
monkeypatch.setattr(routes.threading, "Thread", InlineThread)
|
|
monkeypatch.setattr(routes.Session, "load", classmethod(lambda _cls, sid: current if sid == current.session_id else None))
|
|
monkeypatch.setattr(routes, "_ensure_full_session_before_mutation", lambda sid, session: session)
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"generate_session_title_for_session",
|
|
lambda session: generated.append(session.session_id) or ("Unexpected generated title", "llm", "raw"),
|
|
)
|
|
monkeypatch.setattr(
|
|
routes,
|
|
"_persist_generated_session_title",
|
|
lambda session, title, *, event_reason, require_default_title=False: persisted.append(
|
|
(session.session_id, title, event_reason, require_default_title)
|
|
),
|
|
)
|
|
|
|
routes._queue_generated_title_for_imported_session(
|
|
current,
|
|
{"title": "CLI Session", "source_tag": "cli"},
|
|
)
|
|
|
|
assert generated == []
|
|
assert persisted == []
|
|
|
|
|
|
def test_generated_title_persist_reloads_latest_session_before_saving(tmp_path, monkeypatch):
|
|
session_dir = tmp_path / "sessions"
|
|
session_dir.mkdir()
|
|
cache = OrderedDict()
|
|
|
|
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
|
|
monkeypatch.setattr(models, "SESSIONS", cache, raising=False)
|
|
monkeypatch.setattr(routes, "SESSIONS", cache, raising=False)
|
|
monkeypatch.setattr(routes, "_sync_session_title_to_insights", lambda session: None)
|
|
monkeypatch.setattr(routes, "_publish_session_list_changed", lambda *args, **kwargs: None)
|
|
|
|
stale = models.Session(
|
|
session_id="cli_import_race",
|
|
title="CLI Session",
|
|
workspace=".",
|
|
model="test-model",
|
|
messages=[{"role": "user", "content": "first"}],
|
|
source_tag="cli",
|
|
raw_source="cli",
|
|
session_source="external_agent",
|
|
source_label="CLI",
|
|
)
|
|
stale.save(skip_index=True)
|
|
stale_snapshot = models.Session.load(stale.session_id)
|
|
|
|
latest = models.Session(
|
|
session_id=stale.session_id,
|
|
title="CLI Session",
|
|
workspace=".",
|
|
model="test-model",
|
|
messages=[
|
|
{"role": "user", "content": "first"},
|
|
{"role": "assistant", "content": "reply"},
|
|
{"role": "user", "content": "second"},
|
|
],
|
|
source_tag="cli",
|
|
raw_source="cli",
|
|
session_source="external_agent",
|
|
source_label="CLI",
|
|
)
|
|
latest.save(skip_index=True)
|
|
cache[latest.session_id] = latest
|
|
|
|
saved_title = routes._persist_generated_session_title(
|
|
stale_snapshot,
|
|
"Better imported title",
|
|
event_reason="session_title_regenerate",
|
|
require_default_title=True,
|
|
)
|
|
|
|
reloaded = models.Session.load(stale.session_id)
|
|
assert saved_title == "Better imported title"
|
|
assert reloaded.title == "Better imported title"
|
|
assert len(reloaded.messages) == 3
|
|
assert reloaded.llm_title_generated is True
|
|
assert reloaded.manual_title is False
|
|
assert len(cache[stale.session_id].messages) == 3
|
|
assert stale_snapshot.title == "Better imported title"
|
|
|
|
|
|
def test_regenerate_endpoint_only_blocks_read_only_imported_sessions():
|
|
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 "_get_or_materialize_session(sid)" in block
|
|
assert "except PermissionError:" in block
|
|
|
|
|
|
def test_sessions_ui_keeps_regenerate_action_for_writable_imports():
|
|
regen_idx = SESSIONS_JS.index("api('/api/session/title/regenerate'")
|
|
window = SESSIONS_JS[regen_idx - 500:regen_idx]
|
|
assert "session.is_imported" not in window
|
|
assert "_isReadOnlySession(session)" in SESSIONS_JS
|