mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-27 01:11:40 +00:00
refactor: unify gateway session title sync flow
This commit is contained in:
parent
59d45346ba
commit
3438de3623
8 changed files with 421 additions and 61 deletions
|
|
@ -7,6 +7,7 @@ import pytest
|
|||
|
||||
from agent.title_generator import (
|
||||
generate_title,
|
||||
generate_title_if_missing,
|
||||
auto_title_session,
|
||||
maybe_auto_title,
|
||||
)
|
||||
|
|
@ -89,19 +90,26 @@ class TestAutoTitleSession:
|
|||
def test_skips_if_no_session_db(self):
|
||||
auto_title_session(None, "sess-1", "hi", "hello") # should not crash
|
||||
|
||||
def test_skips_if_title_exists(self):
|
||||
def test_generate_title_if_missing_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")
|
||||
assert generate_title_if_missing(db, "sess-1", "hi", "hello") is None
|
||||
gen.assert_not_called()
|
||||
|
||||
def test_generate_title_if_missing_returns_generated_title(self):
|
||||
db = MagicMock()
|
||||
db.get_session_title.return_value = None
|
||||
|
||||
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_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"):
|
||||
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")
|
||||
|
||||
|
|
@ -109,7 +117,7 @@ class TestAutoTitleSession:
|
|||
db = MagicMock()
|
||||
db.get_session_title.return_value = None
|
||||
|
||||
with patch("agent.title_generator.generate_title", return_value=None):
|
||||
with patch("agent.title_generator.generate_title_if_missing", return_value=None):
|
||||
auto_title_session(db, "sess-1", "hi", "hello")
|
||||
db.set_session_title.assert_not_called()
|
||||
|
||||
|
|
|
|||
|
|
@ -782,6 +782,22 @@ async def test_base_processing_releases_post_delivery_callback_after_main_send()
|
|||
assert released == [True]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_delivery_callbacks_compose_for_same_session():
|
||||
"""Multiple post-delivery callbacks for a session should all run."""
|
||||
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)
|
||||
|
||||
callback = adapter.pop_post_delivery_callback("sess", generation=3)
|
||||
assert callable(callback)
|
||||
callback()
|
||||
|
||||
assert fired == ["first", "second"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agent_drops_tool_progress_after_generation_invalidation(monkeypatch, tmp_path):
|
||||
import yaml
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
"""Tests for /title gateway slash command.
|
||||
"""Tests for gateway session-title flows.
|
||||
|
||||
Tests the _handle_title_command handler (set/show session titles)
|
||||
across all gateway messenger platforms.
|
||||
Tests the /title handler plus native gateway session-title propagation
|
||||
for manual and auto-generated titles.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -33,6 +34,7 @@ def _make_runner(session_db=None):
|
|||
runner.adapters = {}
|
||||
runner._voice_mode = {}
|
||||
runner._session_db = session_db
|
||||
runner._background_tasks = set()
|
||||
|
||||
# Mock session_store that returns a session entry with a known session_id
|
||||
mock_session_entry = MagicMock()
|
||||
|
|
@ -72,7 +74,7 @@ class TestHandleTitleCommand:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_title_renames_telegram_topic_when_in_thread(self, tmp_path):
|
||||
"""Telegram /title should also rename the active topic thread when possible."""
|
||||
"""Telegram /title should schedule thread-title sync via the native callback path."""
|
||||
from hermes_state import SessionDB
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session("test_session_123", "telegram")
|
||||
|
|
@ -85,14 +87,18 @@ class TestHandleTitleCommand:
|
|||
event = _make_event(text="/title Indicative Topic", thread_id="470094")
|
||||
result = await runner._handle_title_command(event)
|
||||
|
||||
adapter.register_post_delivery_callback.assert_called_once()
|
||||
callback = adapter.register_post_delivery_callback.call_args.args[1]
|
||||
callback()
|
||||
await asyncio.sleep(0)
|
||||
adapter.update_thread_title.assert_awaited_once_with("67890", "470094", "Indicative Topic")
|
||||
assert "Telegram topic renamed too" in result
|
||||
assert "Telegram topic renamed too" not in result
|
||||
assert db.get_session_title("test_session_123") == "Indicative Topic"
|
||||
db.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_title_renames_telegram_general_topic_when_thread_is_one(self, tmp_path):
|
||||
"""Telegram General topic thread_id=1 should still trigger a rename attempt."""
|
||||
"""Telegram General topic thread_id=1 should also use the deferred sync path."""
|
||||
from hermes_state import SessionDB
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session("test_session_123", "telegram")
|
||||
|
|
@ -105,8 +111,12 @@ class TestHandleTitleCommand:
|
|||
event = _make_event(text="/title Lobby", thread_id="1")
|
||||
result = await runner._handle_title_command(event)
|
||||
|
||||
adapter.register_post_delivery_callback.assert_called_once()
|
||||
callback = adapter.register_post_delivery_callback.call_args.args[1]
|
||||
callback()
|
||||
await asyncio.sleep(0)
|
||||
adapter.update_thread_title.assert_awaited_once_with("67890", "1", "Lobby")
|
||||
assert "Telegram topic renamed too" in result
|
||||
assert "Telegram topic renamed too" not in result
|
||||
assert db.get_session_title("test_session_123") == "Lobby"
|
||||
db.close()
|
||||
|
||||
|
|
@ -125,11 +135,91 @@ class TestHandleTitleCommand:
|
|||
event = _make_event(text="/title Plain Chat Title")
|
||||
result = await runner._handle_title_command(event)
|
||||
|
||||
adapter.update_thread_title.assert_not_called()
|
||||
assert "Telegram topic renamed too" not in result
|
||||
adapter.register_post_delivery_callback.assert_not_called()
|
||||
assert db.get_session_title("test_session_123") == "Plain Chat Title"
|
||||
db.close()
|
||||
|
||||
|
||||
class TestGatewayAutoTitleSync:
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_title_flow_uses_same_session_title_path(self, tmp_path):
|
||||
"""Gateway auto-title should persist title and sync Telegram thread title."""
|
||||
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)
|
||||
adapter = MagicMock()
|
||||
adapter.update_thread_title = AsyncMock(return_value=True)
|
||||
runner.adapters[Platform.TELEGRAM] = adapter
|
||||
source = _make_event(thread_id="470094").source
|
||||
|
||||
with patch("agent.title_generator.generate_title_if_missing", return_value="Auto Topic"):
|
||||
await runner._auto_title_gateway_session(
|
||||
session_id="test_session_123",
|
||||
session_key="telegram:12345:67890",
|
||||
source=source,
|
||||
user_message="hello",
|
||||
assistant_response="hi there",
|
||||
)
|
||||
|
||||
assert db.get_session_title("test_session_123") == "Auto Topic"
|
||||
adapter.update_thread_title.assert_awaited_once_with("67890", "470094", "Auto Topic")
|
||||
db.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_title_skips_platform_sync_when_no_thread(self, tmp_path):
|
||||
"""Gateway auto-title without a thread should remain DB-only."""
|
||||
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)
|
||||
adapter = MagicMock()
|
||||
adapter.update_thread_title = AsyncMock(return_value=True)
|
||||
runner.adapters[Platform.TELEGRAM] = adapter
|
||||
source = _make_event().source
|
||||
|
||||
with patch("agent.title_generator.generate_title_if_missing", return_value="Auto Session"):
|
||||
await runner._auto_title_gateway_session(
|
||||
session_id="test_session_123",
|
||||
session_key="telegram:12345:67890",
|
||||
source=source,
|
||||
user_message="hello",
|
||||
assistant_response="hi there",
|
||||
)
|
||||
|
||||
assert db.get_session_title("test_session_123") == "Auto Session"
|
||||
adapter.update_thread_title.assert_not_called()
|
||||
db.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_title_skips_overwriting_existing_manual_title(self, tmp_path):
|
||||
"""Gateway auto-title should not clobber a title set while generation was in flight."""
|
||||
from hermes_state import SessionDB
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session("test_session_123", "telegram")
|
||||
db.set_session_title("test_session_123", "Manual Title")
|
||||
|
||||
runner = _make_runner(session_db=db)
|
||||
adapter = MagicMock()
|
||||
adapter.update_thread_title = AsyncMock(return_value=True)
|
||||
runner.adapters[Platform.TELEGRAM] = adapter
|
||||
source = _make_event(thread_id="470094").source
|
||||
|
||||
with patch("agent.title_generator.generate_title_if_missing", return_value="Auto Topic"):
|
||||
await runner._auto_title_gateway_session(
|
||||
session_id="test_session_123",
|
||||
session_key="telegram:12345:67890",
|
||||
source=source,
|
||||
user_message="hello",
|
||||
assistant_response="hi there",
|
||||
)
|
||||
|
||||
assert db.get_session_title("test_session_123") == "Manual Title"
|
||||
adapter.update_thread_title.assert_not_called()
|
||||
db.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_show_title_when_set(self, tmp_path):
|
||||
"""Showing title when one is set returns the title."""
|
||||
|
|
|
|||
|
|
@ -945,6 +945,7 @@ class TestSessionTitle:
|
|||
|
||||
def test_set_title_nonexistent_session(self, db):
|
||||
assert db.set_session_title("nonexistent", "Title") is False
|
||||
assert db.set_session_title_if_missing("nonexistent", "Title") is False
|
||||
|
||||
def test_title_initially_none(self, db):
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
|
|
@ -959,6 +960,20 @@ class TestSessionTitle:
|
|||
session = db.get_session("s1")
|
||||
assert session["title"] == "Updated Title"
|
||||
|
||||
def test_set_title_if_missing_only_sets_once(self, db):
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
assert db.set_session_title_if_missing("s1", "Initial Title") is True
|
||||
assert db.set_session_title_if_missing("s1", "Ignored Title") is False
|
||||
assert db.get_session("s1")["title"] == "Initial Title"
|
||||
|
||||
def test_set_title_if_missing_respects_uniqueness(self, db):
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
db.create_session(session_id="s2", source="cli")
|
||||
db.set_session_title("s1", "Taken")
|
||||
|
||||
with pytest.raises(ValueError, match="already in use"):
|
||||
db.set_session_title_if_missing("s2", "Taken")
|
||||
|
||||
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