""" Tests for the DispatchHook protocol and NullDispatchHook. """ from __future__ import annotations import pytest from xml_pipeline.message_bus.dispatch_hook import DispatchHook, NullDispatchHook class TestNullDispatchHook: """NullDispatchHook should be a no-op that satisfies the protocol.""" async def test_on_intent_returns_empty_string(self): hook = NullDispatchHook() result = await hook.on_intent( thread_id="thread-1", from_id="sender", to_id="receiver", payload_type="Greeting", payload_bytes=b"Alice", ) assert result == "" async def test_on_dispatched_is_noop(self): hook = NullDispatchHook() await hook.on_dispatched("entry-1") # Should not raise async def test_on_acknowledged_is_noop(self): hook = NullDispatchHook() await hook.on_acknowledged("entry-1") # Should not raise async def test_on_failed_is_noop(self): hook = NullDispatchHook() await hook.on_failed("entry-1", "some error") # Should not raise async def test_on_thread_complete_is_noop(self): hook = NullDispatchHook() await hook.on_thread_complete("thread-1") # Should not raise class TestDispatchHookProtocol: """DispatchHook protocol should be runtime checkable.""" def test_null_hook_satisfies_protocol(self): hook = NullDispatchHook() assert isinstance(hook, DispatchHook) def test_custom_hook_satisfies_protocol(self): """A custom class implementing all methods should satisfy the protocol.""" class MyHook: async def on_intent(self, thread_id, from_id, to_id, payload_type, payload_bytes): return "my-id" async def on_dispatched(self, entry_id): pass async def on_acknowledged(self, entry_id): pass async def on_failed(self, entry_id, error): pass async def on_thread_complete(self, thread_id): pass hook = MyHook() assert isinstance(hook, DispatchHook) def test_incomplete_class_fails_protocol(self): """A class missing methods should not satisfy the protocol.""" class IncompleteHook: async def on_intent(self, thread_id, from_id, to_id, payload_type, payload_bytes): return "id" hook = IncompleteHook() assert not isinstance(hook, DispatchHook) class InMemoryHook: """Test hook that records all calls for assertion.""" def __init__(self): self.events = [] async def on_intent(self, thread_id, from_id, to_id, payload_type, payload_bytes): entry_id = f"entry-{len(self.events)}" self.events.append(("intent", entry_id, thread_id, from_id, to_id)) return entry_id async def on_dispatched(self, entry_id): self.events.append(("dispatched", entry_id)) async def on_acknowledged(self, entry_id): self.events.append(("acknowledged", entry_id)) async def on_failed(self, entry_id, error): self.events.append(("failed", entry_id, error)) async def on_thread_complete(self, thread_id): self.events.append(("thread_complete", thread_id)) class TestInMemoryHook: """Test the in-memory hook used for testing.""" async def test_full_lifecycle(self): hook = InMemoryHook() # Intent eid = await hook.on_intent("t1", "sender", "receiver", "Greeting", b"") assert eid == "entry-0" # Dispatched await hook.on_dispatched(eid) # Acknowledged await hook.on_acknowledged(eid) # Thread complete await hook.on_thread_complete("t1") assert len(hook.events) == 4 assert hook.events[0][0] == "intent" assert hook.events[1] == ("dispatched", "entry-0") assert hook.events[2] == ("acknowledged", "entry-0") assert hook.events[3] == ("thread_complete", "t1") async def test_failed_lifecycle(self): hook = InMemoryHook() eid = await hook.on_intent("t1", "sender", "receiver", "Greeting", b"") await hook.on_dispatched(eid) await hook.on_failed(eid, "handler crashed") assert len(hook.events) == 3 assert hook.events[2] == ("failed", "entry-0", "handler crashed") def test_satisfies_protocol(self): hook = InMemoryHook() assert isinstance(hook, DispatchHook)