mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
test: expand title sync coverage
This commit is contained in:
parent
3438de3623
commit
e46dd64876
5 changed files with 240 additions and 0 deletions
|
|
@ -105,6 +105,14 @@ class TestAutoTitleSession:
|
|||
with patch("agent.title_generator.generate_title", return_value="New Title"):
|
||||
assert generate_title_if_missing(db, "sess-1", "hi", "hello") == "New Title"
|
||||
|
||||
def test_generate_title_if_missing_returns_none_on_lookup_error(self):
|
||||
db = MagicMock()
|
||||
db.get_session_title.side_effect = RuntimeError("db unavailable")
|
||||
|
||||
with patch("agent.title_generator.generate_title") as gen:
|
||||
assert generate_title_if_missing(db, "sess-1", "hi", "hello") is None
|
||||
gen.assert_not_called()
|
||||
|
||||
def test_generates_and_sets_title(self):
|
||||
db = MagicMock()
|
||||
db.get_session_title.return_value = None
|
||||
|
|
@ -121,6 +129,14 @@ class TestAutoTitleSession:
|
|||
auto_title_session(db, "sess-1", "hi", "hello")
|
||||
db.set_session_title.assert_not_called()
|
||||
|
||||
def test_swallows_set_title_errors(self):
|
||||
db = MagicMock()
|
||||
db.set_session_title.side_effect = RuntimeError("write failed")
|
||||
|
||||
with patch("agent.title_generator.generate_title_if_missing", return_value="New Title"):
|
||||
auto_title_session(db, "sess-1", "hi", "hello")
|
||||
db.set_session_title.assert_called_once_with("sess-1", "New Title")
|
||||
|
||||
|
||||
class TestMaybeAutoTitle:
|
||||
"""Tests for maybe_auto_title() — the fire-and-forget entry point."""
|
||||
|
|
|
|||
|
|
@ -245,6 +245,28 @@ async def test_update_thread_title_returns_false_on_api_error():
|
|||
assert adapter._get_cached_thread_topic_title("111", "222") is None
|
||||
|
||||
|
||||
def test_cache_thread_topic_title_ignores_incomplete_values():
|
||||
"""Empty title/chat/thread inputs should not populate the runtime cache."""
|
||||
adapter = _make_adapter()
|
||||
|
||||
adapter._cache_thread_topic_title("111", "222", "")
|
||||
adapter._cache_thread_topic_title("", "222", "Title")
|
||||
adapter._cache_thread_topic_title("111", "", "Title")
|
||||
|
||||
assert adapter._thread_topic_titles == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_thread_title_returns_false_without_bot_or_title():
|
||||
"""Missing bot or blank title should short-circuit before any API call."""
|
||||
adapter = _make_adapter()
|
||||
assert await adapter.update_thread_title("111", "222", "Title") is False
|
||||
|
||||
adapter._bot = AsyncMock()
|
||||
assert await adapter.update_thread_title("111", "222", " ") is False
|
||||
adapter._bot.edit_forum_topic.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forum_topic_service_handler_caches_created_and_edited_names():
|
||||
"""Service messages should hot-update the runtime topic-title cache."""
|
||||
|
|
@ -263,6 +285,24 @@ async def test_forum_topic_service_handler_caches_created_and_edited_names():
|
|||
assert adapter._get_cached_thread_topic_title("111", "222") == "Edited Title"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forum_topic_service_handler_ignores_missing_message_or_context():
|
||||
"""Service handler should no-op when update lacks message/thread/chat context."""
|
||||
adapter = _make_adapter()
|
||||
|
||||
await adapter._handle_forum_topic_service_message(SimpleNamespace(message=None), None)
|
||||
await adapter._handle_forum_topic_service_message(
|
||||
SimpleNamespace(message=SimpleNamespace(message_thread_id=None, chat=SimpleNamespace(id=111))),
|
||||
None,
|
||||
)
|
||||
await adapter._handle_forum_topic_service_message(
|
||||
SimpleNamespace(message=SimpleNamespace(message_thread_id=222, chat=None)),
|
||||
None,
|
||||
)
|
||||
|
||||
assert adapter._thread_topic_titles == {}
|
||||
|
||||
|
||||
# ── _persist_dm_topic_thread_id ──
|
||||
|
||||
|
||||
|
|
@ -596,6 +636,37 @@ def test_build_message_event_no_auto_skill_without_thread():
|
|||
assert event.auto_skill is None
|
||||
|
||||
|
||||
def test_build_message_event_prefers_cached_thread_topic_title():
|
||||
"""Runtime-cached topic titles should flow into the built SessionSource."""
|
||||
from gateway.platforms.base import MessageType
|
||||
|
||||
adapter = _make_adapter()
|
||||
adapter._cache_thread_topic_title("111", "200", "Cached Topic")
|
||||
|
||||
msg = _make_mock_message(chat_id=111, thread_id=200)
|
||||
event = adapter._build_message_event(msg, MessageType.TEXT)
|
||||
|
||||
assert event.source.chat_topic == "Cached Topic"
|
||||
|
||||
|
||||
def test_build_message_event_uses_topic_created_and_edited_service_names():
|
||||
"""Forum-topic service payloads should update the event topic name immediately."""
|
||||
from gateway.platforms.base import MessageType
|
||||
|
||||
adapter = _make_adapter()
|
||||
msg = _make_mock_message(
|
||||
chat_id=111,
|
||||
thread_id=300,
|
||||
forum_topic_created=SimpleNamespace(name="Created Topic"),
|
||||
)
|
||||
msg.forum_topic_edited = SimpleNamespace(name="Edited Topic")
|
||||
|
||||
event = adapter._build_message_event(msg, MessageType.TEXT)
|
||||
|
||||
assert event.source.chat_topic == "Edited Topic"
|
||||
assert adapter._get_cached_thread_topic_title("111", "300") == "Edited Topic"
|
||||
|
||||
|
||||
# ── _build_message_event: group_topics skill binding ──
|
||||
|
||||
# The telegram mock sets sys.modules["telegram.constants"] = telegram_mod (root mock),
|
||||
|
|
|
|||
|
|
@ -798,6 +798,43 @@ async def test_post_delivery_callbacks_compose_for_same_session():
|
|||
assert fired == ["first", "second"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_delivery_callbacks_append_to_existing_list_and_preserve_unmatched_generation():
|
||||
"""Generation filtering should leave unmatched callbacks queued for a later pop."""
|
||||
adapter = ProgressCaptureAdapter()
|
||||
fired = []
|
||||
|
||||
adapter.register_post_delivery_callback("sess", lambda: fired.append("first"), generation=3)
|
||||
adapter.register_post_delivery_callback("sess", lambda: fired.append("second"), generation=3)
|
||||
adapter.register_post_delivery_callback("sess", lambda: fired.append("third"), generation=4)
|
||||
|
||||
callback = adapter.pop_post_delivery_callback("sess", generation=3)
|
||||
assert callable(callback)
|
||||
callback()
|
||||
assert fired == ["first", "second"]
|
||||
|
||||
callback = adapter.pop_post_delivery_callback("sess", generation=4)
|
||||
assert callable(callback)
|
||||
callback()
|
||||
assert fired == ["first", "second", "third"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_delivery_callback_plain_entry_survives_generation_filtered_pop():
|
||||
"""A plain callback should remain queued when popping with a generation filter."""
|
||||
adapter = ProgressCaptureAdapter()
|
||||
fired = []
|
||||
|
||||
adapter.register_post_delivery_callback("sess", lambda: fired.append("plain"))
|
||||
|
||||
assert adapter.pop_post_delivery_callback("sess", generation=7) is None
|
||||
callback = adapter.pop_post_delivery_callback("sess")
|
||||
assert callable(callback)
|
||||
callback()
|
||||
|
||||
assert fired == ["plain"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agent_drops_tool_progress_after_generation_invalidation(monkeypatch, tmp_path):
|
||||
import yaml
|
||||
|
|
|
|||
|
|
@ -220,6 +220,116 @@ class TestGatewayAutoTitleSync:
|
|||
adapter.update_thread_title.assert_not_called()
|
||||
db.close()
|
||||
|
||||
|
||||
class TestGatewayTitleHelpers:
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_session_title_to_source_returns_false_without_adapter_or_updater(self):
|
||||
runner = _make_runner()
|
||||
source = _make_event(thread_id="470094").source
|
||||
|
||||
assert await runner._sync_session_title_to_source(source, "Title") is False
|
||||
|
||||
runner.adapters[Platform.TELEGRAM] = MagicMock()
|
||||
assert await runner._sync_session_title_to_source(source, "Title") is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_session_title_to_source_handles_updater_errors(self):
|
||||
runner = _make_runner()
|
||||
source = _make_event(thread_id="470094").source
|
||||
adapter = MagicMock()
|
||||
adapter.update_thread_title = AsyncMock(side_effect=RuntimeError("boom"))
|
||||
runner.adapters[Platform.TELEGRAM] = adapter
|
||||
|
||||
assert await runner._sync_session_title_to_source(source, "Title") is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_schedule_session_title_sync_after_delivery_falls_back_to_adapter_dict(self):
|
||||
runner = _make_runner()
|
||||
source = _make_event(thread_id="470094").source
|
||||
adapter = MagicMock()
|
||||
adapter._post_delivery_callbacks = {}
|
||||
del adapter.register_post_delivery_callback
|
||||
adapter.update_thread_title = AsyncMock(return_value=True)
|
||||
runner.adapters[Platform.TELEGRAM] = adapter
|
||||
|
||||
scheduled = runner._schedule_session_title_sync_after_delivery(
|
||||
session_key="telegram:12345:67890",
|
||||
source=source,
|
||||
title="Title",
|
||||
)
|
||||
|
||||
assert scheduled is True
|
||||
callback = adapter._post_delivery_callbacks["telegram:12345:67890"]
|
||||
callback()
|
||||
await asyncio.sleep(0)
|
||||
adapter.update_thread_title.assert_awaited_once_with("67890", "470094", "Title")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_session_title_uses_only_if_missing_path(self, tmp_path):
|
||||
from hermes_state import SessionDB
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session("test_session_123", "telegram")
|
||||
|
||||
runner = _make_runner(session_db=db)
|
||||
source = _make_event(thread_id="470094").source
|
||||
|
||||
changed = await runner._apply_session_title(
|
||||
session_id="test_session_123",
|
||||
source=source,
|
||||
title="Initial",
|
||||
only_if_missing=True,
|
||||
)
|
||||
unchanged = await runner._apply_session_title(
|
||||
session_id="test_session_123",
|
||||
source=source,
|
||||
title="Second",
|
||||
only_if_missing=True,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
assert unchanged is False
|
||||
assert db.get_session_title("test_session_123") == "Initial"
|
||||
db.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maybe_schedule_gateway_auto_title_registers_post_delivery_callback(self):
|
||||
runner = _make_runner(session_db=MagicMock())
|
||||
source = _make_event(thread_id="470094").source
|
||||
adapter = MagicMock()
|
||||
runner.adapters[Platform.TELEGRAM] = adapter
|
||||
|
||||
with patch("agent.title_generator.should_auto_title", return_value=True):
|
||||
runner._maybe_schedule_gateway_auto_title(
|
||||
session_id="test_session_123",
|
||||
session_key="telegram:12345:67890",
|
||||
source=source,
|
||||
user_message="hello",
|
||||
assistant_response="hi",
|
||||
conversation_history=[{"role": "user", "content": "hello"}],
|
||||
generation=5,
|
||||
)
|
||||
|
||||
adapter.register_post_delivery_callback.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_maybe_schedule_gateway_auto_title_launches_immediately_without_adapter_registration(self):
|
||||
runner = _make_runner(session_db=MagicMock())
|
||||
source = _make_event(thread_id="470094").source
|
||||
|
||||
with patch("agent.title_generator.should_auto_title", return_value=True), \
|
||||
patch.object(runner, "_auto_title_gateway_session", AsyncMock()) as auto_title:
|
||||
runner._maybe_schedule_gateway_auto_title(
|
||||
session_id="test_session_123",
|
||||
session_key=None,
|
||||
source=source,
|
||||
user_message="hello",
|
||||
assistant_response="hi",
|
||||
conversation_history=[{"role": "user", "content": "hello"}],
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
auto_title.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_show_title_when_set(self, tmp_path):
|
||||
"""Showing title when one is set returns the title."""
|
||||
|
|
|
|||
|
|
@ -974,6 +974,12 @@ class TestSessionTitle:
|
|||
with pytest.raises(ValueError, match="already in use"):
|
||||
db.set_session_title_if_missing("s2", "Taken")
|
||||
|
||||
def test_set_title_if_missing_rejects_empty_title(self, db):
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
|
||||
assert db.set_session_title_if_missing("s1", " ") is False
|
||||
assert db.get_session("s1")["title"] is None
|
||||
|
||||
def test_title_in_search_sessions(self, db):
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
db.set_session_title("s1", "Debugging Auth")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue