"""
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)