hermes-agent/tests/gateway/test_session_dm_thread_seeding.py
kshitijk4poor 66827f8947 chore: prune unused imports and duplicate import redefinitions
Remove unused imports (F401) and duplicate/shadowed import
redefinitions (F811) across the codebase using ruff's safe
autofixes. No behavioral changes -- imports only.

- ~1400 safe autofixes applied across 644 files (net -1072 lines)
- __init__.py re-exports preserved (excluded from F401 removal so
  public re-export surfaces stay intact)
- Re-exports that are imported or monkeypatched by tests but look
  unused in their defining module are kept with explicit # noqa:
  F401 (gateway/run.py load_dotenv; run_agent re-exports from
  agent.message_sanitization, agent.context_compressor,
  agent.retry_utils, agent.prompt_builder, agent.process_bootstrap,
  agent.codex_responses_adapter)
- Unsafe F841 (unused-variable) fixes deliberately skipped -- those
  can change behavior when the RHS has side effects
- ruff lints remain disabled in pyproject.toml (only PLW1514 is
  selected); this is a one-time cleanup, not a config change

Verification:
- python -m compileall: clean
- pytest --collect-only: all 27161 tests collect (zero import errors)
- core entry points import clean (run_agent, model_tools, cli,
  toolsets, hermes_state, batch_runner, gateway)
- static scan: every name any test imports directly from an edited
  module still resolves
2026-05-28 22:26:25 -07:00

196 lines
7.9 KiB
Python

"""Tests for DM thread session isolation.
DM thread sessions must start empty — no parent transcript seeding.
Thread context is handled by platform adapters (e.g. Slack's
_fetch_thread_context fetches actual thread replies via the API).
Session-level seeding was removed because it copied the ENTIRE parent
DM transcript, causing unrelated conversations to bleed across threads.
Covers:
- Thread sessions start empty (no parent seeding)
- Group/channel thread sessions also start empty
- Multiple threads from same parent are independent
- Existing thread sessions are not mutated on re-access
- Cross-platform: consistent behavior for Slack, Telegram, Discord
"""
import pytest
from gateway.config import Platform, GatewayConfig
from gateway.session import SessionSource, SessionStore
@pytest.fixture()
def store(tmp_path, monkeypatch):
"""SessionStore with SQLite — load_transcript reads from DB only.
Pin DEFAULT_DB_PATH to tmp_path so SessionDB() can't write to the real
~/.hermes/state.db. (DEFAULT_DB_PATH is a module-level constant computed
at hermes_state import time, before pytest's HERMES_HOME monkeypatch
fires — the autouse fixture's HERMES_HOME override doesn't help here.)
"""
import hermes_state
monkeypatch.setattr(hermes_state, "DEFAULT_DB_PATH", tmp_path / "state.db")
config = GatewayConfig()
s = SessionStore(sessions_dir=tmp_path, config=config)
return s
def _dm_source(platform=Platform.SLACK, chat_id="D123", thread_id=None, user_id="U1"):
return SessionSource(
platform=platform,
chat_id=chat_id,
chat_type="dm",
user_id=user_id,
thread_id=thread_id,
)
def _group_source(platform=Platform.SLACK, chat_id="C456", thread_id=None, user_id="U1"):
return SessionSource(
platform=platform,
chat_id=chat_id,
chat_type="group",
user_id=user_id,
thread_id=thread_id,
)
PARENT_HISTORY = [
{"role": "user", "content": "What's the weather?"},
{"role": "assistant", "content": "It's sunny and 72°F."},
]
class TestDMThreadIsolation:
"""Thread sessions must start empty — no parent transcript seeding."""
def test_thread_session_starts_empty(self, store):
"""New DM thread session should NOT inherit parent's transcript."""
parent_source = _dm_source()
parent_entry = store.get_or_create_session(parent_source)
for msg in PARENT_HISTORY:
store.append_to_transcript(parent_entry.session_id, msg)
thread_source = _dm_source(thread_id="1234567890.000001")
thread_entry = store.get_or_create_session(thread_source)
thread_transcript = store.load_transcript(thread_entry.session_id)
assert len(thread_transcript) == 0
def test_parent_transcript_unaffected_by_thread(self, store):
"""Creating a thread session should not alter parent's transcript."""
parent_source = _dm_source()
parent_entry = store.get_or_create_session(parent_source)
for msg in PARENT_HISTORY:
store.append_to_transcript(parent_entry.session_id, msg)
thread_source = _dm_source(thread_id="1234567890.000001")
thread_entry = store.get_or_create_session(thread_source)
store.append_to_transcript(thread_entry.session_id, {
"role": "user", "content": "thread-only message"
})
parent_transcript = store.load_transcript(parent_entry.session_id)
assert len(parent_transcript) == 2
assert all(m["content"] != "thread-only message" for m in parent_transcript)
def test_multiple_threads_are_independent(self, store):
"""Each thread from the same parent starts empty and stays independent."""
parent_source = _dm_source()
parent_entry = store.get_or_create_session(parent_source)
for msg in PARENT_HISTORY:
store.append_to_transcript(parent_entry.session_id, msg)
# Thread A
thread_a_source = _dm_source(thread_id="1111.000001")
thread_a_entry = store.get_or_create_session(thread_a_source)
store.append_to_transcript(thread_a_entry.session_id, {
"role": "user", "content": "thread A message"
})
# Thread B
thread_b_source = _dm_source(thread_id="2222.000002")
thread_b_entry = store.get_or_create_session(thread_b_source)
# Thread B starts empty
thread_b_transcript = store.load_transcript(thread_b_entry.session_id)
assert len(thread_b_transcript) == 0
# Thread A has only its own message
thread_a_transcript = store.load_transcript(thread_a_entry.session_id)
assert len(thread_a_transcript) == 1
assert thread_a_transcript[0]["content"] == "thread A message"
def test_existing_thread_session_preserved(self, store):
"""Returning to an existing thread session should not reset it."""
parent_source = _dm_source()
parent_entry = store.get_or_create_session(parent_source)
for msg in PARENT_HISTORY:
store.append_to_transcript(parent_entry.session_id, msg)
thread_source = _dm_source(thread_id="1234567890.000001")
thread_entry = store.get_or_create_session(thread_source)
store.append_to_transcript(thread_entry.session_id, {
"role": "user", "content": "follow-up"
})
# Get the same thread session again
thread_entry_again = store.get_or_create_session(thread_source)
assert thread_entry_again.session_id == thread_entry.session_id
# Should still have only its own message
thread_transcript = store.load_transcript(thread_entry_again.session_id)
assert len(thread_transcript) == 1
assert thread_transcript[0]["content"] == "follow-up"
class TestDMThreadIsolationEdgeCases:
"""Edge cases — threads always start empty regardless of context."""
def test_group_thread_starts_empty(self, store):
"""Group/channel threads should also start empty."""
parent_source = _group_source()
parent_entry = store.get_or_create_session(parent_source)
for msg in PARENT_HISTORY:
store.append_to_transcript(parent_entry.session_id, msg)
thread_source = _group_source(thread_id="1234567890.000001")
thread_entry = store.get_or_create_session(thread_source)
thread_transcript = store.load_transcript(thread_entry.session_id)
assert len(thread_transcript) == 0
def test_thread_without_parent_session_starts_empty(self, store):
"""Thread session without a parent DM session should start empty."""
thread_source = _dm_source(thread_id="1234567890.000001")
thread_entry = store.get_or_create_session(thread_source)
thread_transcript = store.load_transcript(thread_entry.session_id)
assert len(thread_transcript) == 0
def test_dm_without_thread_starts_empty(self, store):
"""Top-level DMs (no thread_id) should start empty as always."""
source = _dm_source()
entry = store.get_or_create_session(source)
transcript = store.load_transcript(entry.session_id)
assert len(transcript) == 0
class TestDMThreadIsolationCrossPlatform:
"""Verify thread isolation is consistent across all platforms."""
@pytest.mark.parametrize("platform", [Platform.SLACK, Platform.TELEGRAM, Platform.DISCORD])
def test_thread_starts_empty_across_platforms(self, store, platform):
"""DM thread sessions start empty regardless of platform."""
parent_source = _dm_source(platform=platform)
parent_entry = store.get_or_create_session(parent_source)
for msg in PARENT_HISTORY:
store.append_to_transcript(parent_entry.session_id, msg)
thread_source = _dm_source(platform=platform, thread_id="thread_123")
thread_entry = store.get_or_create_session(thread_source)
thread_transcript = store.load_transcript(thread_entry.session_id)
assert len(thread_transcript) == 0