from __future__ import annotations from datetime import datetime import pytest from yuxi.channels.thread_binding_manager import ( BindingType, ThreadBinding, ThreadBindingManager, ) class TestBindingType: def test_agent(self): assert BindingType.AGENT == "agent" def test_subagent(self): assert BindingType.SUBAGENT == "subagent" def test_acp(self): assert BindingType.ACP == "acp" class TestThreadBinding: def test_creation(self): binding = ThreadBinding( thread_id="thread_1", binding_type=BindingType.AGENT, target_id="agent_1", ) assert binding.thread_id == "thread_1" assert binding.binding_type == BindingType.AGENT assert binding.target_id == "agent_1" assert binding.expires_at is None assert binding.is_expired is False def test_not_expired_without_expiry(self): binding = ThreadBinding( thread_id="t1", binding_type=BindingType.AGENT, target_id="a1", ) assert binding.is_expired is False def test_expired(self): from datetime import timedelta from yuxi.utils.datetime_utils import utc_now_naive binding = ThreadBinding( thread_id="t1", binding_type=BindingType.AGENT, target_id="a1", expires_at=utc_now_naive() - timedelta(hours=1), ) assert binding.is_expired is True def test_default_metadata(self): binding = ThreadBinding( thread_id="t1", binding_type=BindingType.AGENT, target_id="a1", ) assert binding.metadata == {} class TestThreadBindingManager: def test_bind_success(self): mgr = ThreadBindingManager(default_ttl_hours=None) binding = mgr.bind("thread_1", BindingType.AGENT, "agent_1") assert binding.thread_id == "thread_1" assert binding.target_id == "agent_1" def test_bind_and_get(self): mgr = ThreadBindingManager(default_ttl_hours=None) mgr.bind("thread_1", BindingType.AGENT, "agent_1") result = mgr.get_binding("thread_1", BindingType.AGENT) assert result is not None assert result.target_id == "agent_1" def test_get_nonexistent(self): mgr = ThreadBindingManager() result = mgr.get_binding("nonexistent", BindingType.AGENT) assert result is None def test_unbind(self): mgr = ThreadBindingManager(default_ttl_hours=None) mgr.bind("thread_1", BindingType.AGENT, "agent_1") assert mgr.unbind("thread_1", BindingType.AGENT) is True assert mgr.get_binding("thread_1", BindingType.AGENT) is None def test_unbind_nonexistent(self): mgr = ThreadBindingManager() assert mgr.unbind("nonexistent", BindingType.AGENT) is False def test_list_bindings(self): mgr = ThreadBindingManager(default_ttl_hours=None) mgr.bind("thread_1", BindingType.AGENT, "agent_1") mgr.bind("thread_1", BindingType.SUBAGENT, "sub_1") mgr.bind("thread_2", BindingType.AGENT, "agent_2") all_bindings = mgr.list_bindings() assert len(all_bindings) == 3 thread1_bindings = mgr.list_bindings(thread_id="thread_1") assert len(thread1_bindings) == 2 agent_bindings = mgr.list_bindings(binding_type=BindingType.AGENT) assert len(agent_bindings) == 2 def test_bind_with_ttl(self): mgr = ThreadBindingManager(default_ttl_hours=None) binding = mgr.bind("thread_1", BindingType.AGENT, "agent_1", ttl_hours=24) assert binding.expires_at is not None def test_bind_with_default_ttl(self): mgr = ThreadBindingManager(default_ttl_hours=24) binding = mgr.bind("thread_1", BindingType.AGENT, "agent_1") assert binding.expires_at is not None def test_bind_with_metadata(self): mgr = ThreadBindingManager(default_ttl_hours=None) binding = mgr.bind("thread_1", BindingType.AGENT, "agent_1", metadata={"key": "val"}) assert binding.metadata["key"] == "val" def test_listener_notification(self): events: list[tuple[str, str]] = [] def listener(event: str, binding: ThreadBinding): events.append((event, binding.thread_id)) mgr = ThreadBindingManager(default_ttl_hours=None) mgr.add_listener(listener) mgr.bind("thread_1", BindingType.AGENT, "agent_1") mgr.unbind("thread_1", BindingType.AGENT) assert len(events) == 2 assert events[0][0] == "bind" assert events[1][0] == "unbind" def test_remove_listener(self): events: list[tuple[str, str]] = [] def listener(event: str, binding: ThreadBinding): events.append((event, binding.thread_id)) mgr = ThreadBindingManager(default_ttl_hours=None) mgr.add_listener(listener) mgr.remove_listener(listener) mgr.bind("thread_1", BindingType.AGENT, "agent_1") assert len(events) == 0 def test_listener_exception_does_not_crash(self): def faulty_listener(event: str, binding: ThreadBinding): raise RuntimeError("test error") mgr = ThreadBindingManager(default_ttl_hours=None) mgr.add_listener(faulty_listener) binding = mgr.bind("thread_1", BindingType.AGENT, "agent_1") assert binding is not None