| """Tests for agent.title_generator — auto-generated session titles.""" |
|
|
| import threading |
| from unittest.mock import MagicMock, patch |
|
|
| import pytest |
|
|
| from agent.title_generator import ( |
| generate_title, |
| auto_title_session, |
| maybe_auto_title, |
| ) |
|
|
|
|
| class TestGenerateTitle: |
| """Unit tests for generate_title().""" |
|
|
| def test_returns_title_on_success(self): |
| mock_response = MagicMock() |
| mock_response.choices = [MagicMock()] |
| mock_response.choices[0].message.content = "Debugging Python Import Errors" |
|
|
| with patch("agent.title_generator.call_llm", return_value=mock_response): |
| title = generate_title("help me fix this import", "Sure, let me check...") |
| assert title == "Debugging Python Import Errors" |
|
|
| def test_strips_quotes(self): |
| mock_response = MagicMock() |
| mock_response.choices = [MagicMock()] |
| mock_response.choices[0].message.content = '"Setting Up Docker Environment"' |
|
|
| with patch("agent.title_generator.call_llm", return_value=mock_response): |
| title = generate_title("how do I set up docker", "First install...") |
| assert title == "Setting Up Docker Environment" |
|
|
| def test_strips_title_prefix(self): |
| mock_response = MagicMock() |
| mock_response.choices = [MagicMock()] |
| mock_response.choices[0].message.content = "Title: Kubernetes Pod Debugging" |
|
|
| with patch("agent.title_generator.call_llm", return_value=mock_response): |
| title = generate_title("my pod keeps crashing", "Let me look...") |
| assert title == "Kubernetes Pod Debugging" |
|
|
| def test_truncates_long_titles(self): |
| mock_response = MagicMock() |
| mock_response.choices = [MagicMock()] |
| mock_response.choices[0].message.content = "A" * 100 |
|
|
| with patch("agent.title_generator.call_llm", return_value=mock_response): |
| title = generate_title("question", "answer") |
| assert len(title) == 80 |
| assert title.endswith("...") |
|
|
| def test_returns_none_on_empty_response(self): |
| mock_response = MagicMock() |
| mock_response.choices = [MagicMock()] |
| mock_response.choices[0].message.content = "" |
|
|
| with patch("agent.title_generator.call_llm", return_value=mock_response): |
| assert generate_title("question", "answer") is None |
|
|
| def test_returns_none_on_exception(self): |
| with patch("agent.title_generator.call_llm", side_effect=RuntimeError("no provider")): |
| assert generate_title("question", "answer") is None |
|
|
| def test_truncates_long_messages(self): |
| """Long user/assistant messages should be truncated in the LLM request.""" |
| captured_kwargs = {} |
|
|
| def mock_call_llm(**kwargs): |
| captured_kwargs.update(kwargs) |
| resp = MagicMock() |
| resp.choices = [MagicMock()] |
| resp.choices[0].message.content = "Short Title" |
| return resp |
|
|
| with patch("agent.title_generator.call_llm", side_effect=mock_call_llm): |
| generate_title("x" * 1000, "y" * 1000) |
|
|
| |
| user_content = captured_kwargs["messages"][1]["content"] |
| assert len(user_content) < 1100 |
|
|
|
|
| class TestAutoTitleSession: |
| """Tests for auto_title_session() — the sync worker function.""" |
|
|
| def test_skips_if_no_session_db(self): |
| auto_title_session(None, "sess-1", "hi", "hello") |
|
|
| def test_skips_if_title_exists(self): |
| db = MagicMock() |
| db.get_session_title.return_value = "Existing Title" |
|
|
| with patch("agent.title_generator.generate_title") as gen: |
| auto_title_session(db, "sess-1", "hi", "hello") |
| gen.assert_not_called() |
|
|
| def test_generates_and_sets_title(self): |
| db = MagicMock() |
| db.get_session_title.return_value = None |
|
|
| with patch("agent.title_generator.generate_title", return_value="New Title"): |
| auto_title_session(db, "sess-1", "hi", "hello") |
| db.set_session_title.assert_called_once_with("sess-1", "New Title") |
|
|
| def test_skips_if_generation_fails(self): |
| db = MagicMock() |
| db.get_session_title.return_value = None |
|
|
| with patch("agent.title_generator.generate_title", return_value=None): |
| auto_title_session(db, "sess-1", "hi", "hello") |
| db.set_session_title.assert_not_called() |
|
|
|
|
| class TestMaybeAutoTitle: |
| """Tests for maybe_auto_title() — the fire-and-forget entry point.""" |
|
|
| def test_skips_if_not_first_exchange(self): |
| """Should not fire for conversations with more than 2 user messages.""" |
| db = MagicMock() |
| history = [ |
| {"role": "user", "content": "first"}, |
| {"role": "assistant", "content": "response 1"}, |
| {"role": "user", "content": "second"}, |
| {"role": "assistant", "content": "response 2"}, |
| {"role": "user", "content": "third"}, |
| {"role": "assistant", "content": "response 3"}, |
| ] |
|
|
| with patch("agent.title_generator.auto_title_session") as mock_auto: |
| maybe_auto_title(db, "sess-1", "third", "response 3", history) |
| |
| import time |
| time.sleep(0.1) |
| mock_auto.assert_not_called() |
|
|
| def test_fires_on_first_exchange(self): |
| """Should fire a background thread for the first exchange.""" |
| db = MagicMock() |
| db.get_session_title.return_value = None |
| history = [ |
| {"role": "user", "content": "hello"}, |
| {"role": "assistant", "content": "hi there"}, |
| ] |
|
|
| with patch("agent.title_generator.auto_title_session") as mock_auto: |
| maybe_auto_title(db, "sess-1", "hello", "hi there", history) |
| |
| import time |
| time.sleep(0.3) |
| mock_auto.assert_called_once_with(db, "sess-1", "hello", "hi there") |
|
|
| def test_skips_if_no_response(self): |
| db = MagicMock() |
| maybe_auto_title(db, "sess-1", "hello", "", []) |
|
|
| def test_skips_if_no_session_db(self): |
| maybe_auto_title(None, "sess-1", "hello", "response", []) |
|
|