Coverage for agentos/tests/test_event_bus.py: 0%
279 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
1"""Tests for agentos.core.event_bus — EventBus, Subscription, DeadLetter."""
3import asyncio
4import pytest
5from agentos.core.event_bus import (
6 DeadLetter,
7 Event,
8 EventBus,
9 EventPriority,
10 Subscription,
11 default_bus,
12 event,
13)
16# ============================================================================
17# EventPriority
18# ============================================================================
20class TestEventPriority:
21 def test_enum_values(self):
22 assert EventPriority.LOW == 0
23 assert EventPriority.NORMAL == 50
24 assert EventPriority.HIGH == 100
25 assert EventPriority.CRITICAL == 200
27 def test_ordering(self):
28 assert EventPriority.CRITICAL > EventPriority.HIGH > EventPriority.NORMAL > EventPriority.LOW
31# ============================================================================
32# Event
33# ============================================================================
35class TestEvent:
36 def test_defaults(self):
37 e = Event(topic="test")
38 assert e.topic == "test"
39 assert e.payload is None
40 assert isinstance(e.event_id, str)
41 assert len(e.event_id) == 12
42 assert e.priority == EventPriority.NORMAL
43 assert e.source == ""
44 assert e.correlation_id == ""
45 assert e.metadata == {}
47 def test_custom_values(self):
48 e = Event(
49 topic="order.created",
50 payload={"id": 1},
51 priority=EventPriority.HIGH,
52 source="api",
53 correlation_id="abc",
54 metadata={"key": "val"},
55 )
56 assert e.topic == "order.created"
57 assert e.payload == {"id": 1}
58 assert e.priority == EventPriority.HIGH
59 assert e.source == "api"
61 def test_unique_event_ids(self):
62 e1 = Event(topic="a")
63 e2 = Event(topic="b")
64 assert e1.event_id != e2.event_id
66 def test_event_factory(self):
67 e = event("agent.start", payload={"v": 1}, source="scheduler")
68 assert isinstance(e, Event)
69 assert e.topic == "agent.start"
70 assert e.payload == {"v": 1}
73# ============================================================================
74# DeadLetter
75# ============================================================================
77class TestDeadLetter:
78 def test_defaults(self):
79 e = Event(topic="x")
80 dl = DeadLetter(event=e, handler_name="h", error="oops")
81 assert dl.event is e
82 assert dl.handler_name == "h"
83 assert dl.error == "oops"
84 assert dl.retry_count == 0
87# ============================================================================
88# Subscription
89# ============================================================================
91class TestSubscription:
92 async def _noop(self, e): pass
94 def test_matches_exact(self):
95 sub = Subscription(
96 topic_pattern="agent.start",
97 handler=self._noop,
98 handler_name="test",
99 is_pattern=False,
100 )
101 assert sub.matches("agent.start") is True
102 assert sub.matches("agent.stop") is False
104 def test_matches_wildcard(self):
105 sub = Subscription(
106 topic_pattern="agent.*",
107 handler=self._noop,
108 handler_name="test",
109 is_pattern=True,
110 )
111 assert sub.matches("agent.start") is True
112 assert sub.matches("agent.stop") is True
113 assert sub.matches("agent") is False
114 assert sub.matches("other.thing") is False
116 def test_matches_complex_wildcard(self):
117 sub = Subscription(
118 topic_pattern="order.*.created",
119 handler=self._noop,
120 handler_name="test",
121 is_pattern=True,
122 )
123 assert sub.matches("order.123.created") is True
124 assert sub.matches("order.abc.created") is True
126 def test_concurrency_default(self):
127 sub = Subscription(topic_pattern="t", handler=self._noop, handler_name="n")
128 assert sub.concurrency == 1
131# ============================================================================
132# EventBus — Basic
133# ============================================================================
135class TestEventBusBasic:
136 def test_defaults(self):
137 bus = EventBus()
138 assert bus.queue_size == 0
139 assert bus.total_events == 0
140 assert bus.subscription_count == 0
142 def test_custom_params(self):
143 bus = EventBus(max_queue_size=50, dlq_enabled=False, dlq_max_size=200, worker_count=2)
144 assert bus._worker_count == 2
146 def test_list_topics(self):
147 bus = EventBus()
148 assert bus.list_topics() == []
151# ============================================================================
152# EventBus — Subscribe / Unsubscribe
153# ============================================================================
155class TestEventBusSubscriptions:
156 async def _noop(self, e): pass
158 def test_subscribe(self):
159 bus = EventBus()
160 sub = bus.subscribe("agent.start", self._noop, handler_name="h")
161 assert isinstance(sub, Subscription)
162 assert bus.subscription_count == 1
163 assert bus.list_topics() == ["agent.start"]
165 def test_subscribe_auto_name(self):
166 bus = EventBus()
167 bus.subscribe("t", self._noop)
168 assert bus.subscription_count == 1
170 def test_unsubscribe(self):
171 bus = EventBus()
172 bus.subscribe("agent.start", self._noop, handler_name="h")
173 assert bus.unsubscribe("agent.start", "h") is True
174 assert bus.subscription_count == 0
176 def test_unsubscribe_missing(self):
177 bus = EventBus()
178 assert bus.unsubscribe("x", "y") is False
180 def test_unsubscribe_all(self):
181 bus = EventBus()
182 bus.subscribe("a", self._noop, handler_name="h")
183 bus.subscribe("b", self._noop, handler_name="h")
184 assert bus.unsubscribe_all("h") == 2
185 assert bus.subscription_count == 0
188# ============================================================================
189# EventBus — Publish / Process
190# ============================================================================
192class TestEventBusPublish:
193 @pytest.mark.asyncio
194 async def test_publish_dispatch(self):
195 bus = EventBus()
196 received = []
198 async def handler(e):
199 received.append(e.payload)
201 bus.subscribe("test", handler, handler_name="h")
202 await bus.start()
203 e = Event(topic="test", payload="hello")
204 count = await bus.publish(e)
205 await asyncio.sleep(0.1)
206 await bus.stop()
207 assert count == 1
208 assert received == ["hello"]
210 @pytest.mark.asyncio
211 async def test_publish_no_subscribers(self):
212 bus = EventBus()
213 await bus.start()
214 e = Event(topic="no.subs")
215 count = await bus.publish(e)
216 await bus.stop()
217 assert count == 0
219 @pytest.mark.asyncio
220 async def test_publish_wildcard(self):
221 bus = EventBus()
222 received = []
224 async def handler(e):
225 received.append(e.topic)
227 bus.subscribe("agent.*", handler, handler_name="h")
228 await bus.start()
229 await bus.publish(Event(topic="agent.start"))
230 await bus.publish(Event(topic="agent.stop"))
231 await asyncio.sleep(0.1)
232 await bus.stop()
233 assert len(received) == 2
235 @pytest.mark.asyncio
236 async def test_publish_nowait(self):
237 bus = EventBus(max_queue_size=100)
238 received = []
240 async def handler(e):
241 received.append(e.payload)
243 bus.subscribe("t", handler, handler_name="h")
244 await bus.start()
245 count = await bus.publish_nowait(Event(topic="t", payload="x"))
246 await asyncio.sleep(0.1)
247 await bus.stop()
248 assert count == 1
250 @pytest.mark.asyncio
251 async def test_emit_sync(self):
252 bus = EventBus()
253 received = []
255 async def handler(e):
256 received.append(e.payload)
258 bus.subscribe("t", handler, handler_name="h")
259 await bus.start()
260 bus.emit_sync(Event(topic="t", payload="sync"))
261 await asyncio.sleep(0.1)
262 await bus.stop()
263 assert "sync" in received
265 @pytest.mark.asyncio
266 async def test_total_events(self):
267 bus = EventBus()
269 async def handler(e): pass
270 bus.subscribe("t", handler, handler_name="h")
271 await bus.start()
272 await bus.publish(Event(topic="t"))
273 await asyncio.sleep(0.1)
274 await bus.stop()
275 assert bus.total_events >= 1
278# ============================================================================
279# EventBus — Priority ordering
280# ============================================================================
282class TestEventBusPriority:
283 @pytest.mark.asyncio
284 async def test_priority_sorting(self):
285 bus = EventBus()
286 order = []
288 async def handler(e):
289 order.append(e.priority)
290 # Small delay to ensure async processing interleaving
291 await asyncio.sleep(0.01)
293 bus.subscribe("t", handler, handler_name="h")
294 await bus.start()
296 await bus.publish(Event(topic="t", priority=EventPriority.NORMAL))
297 await bus.publish(Event(topic="t", priority=EventPriority.CRITICAL))
298 await asyncio.sleep(0.5)
299 await bus.stop()
301 # At least one event was processed
302 assert len(order) >= 1
305# ============================================================================
306# EventBus — DLQ
307# ============================================================================
309class TestEventBusDLQ:
310 @pytest.mark.asyncio
311 async def test_failed_handler_goes_to_dlq(self):
312 bus = EventBus(dlq_enabled=True)
314 async def failing_handler(e):
315 raise ValueError("boom")
317 bus.subscribe("t", failing_handler, handler_name="h")
318 await bus.start()
319 await bus.publish(Event(topic="t"))
320 await asyncio.sleep(0.2)
321 await bus.stop()
323 dlq = bus.get_dlq()
324 assert len(dlq) == 1
325 assert dlq[0].handler_name == "h"
326 assert "boom" in dlq[0].error
328 @pytest.mark.asyncio
329 async def test_dlq_disabled(self):
330 bus = EventBus(dlq_enabled=False)
332 async def failing_handler(e):
333 raise ValueError("boom")
335 bus.subscribe("t", failing_handler, handler_name="h")
336 await bus.start()
337 await bus.publish(Event(topic="t"))
338 await asyncio.sleep(0.2)
339 await bus.stop()
341 assert len(bus.get_dlq()) == 0
343 @pytest.mark.asyncio
344 async def test_dlq_max_size(self):
345 bus = EventBus(dlq_enabled=True, dlq_max_size=2)
347 async def failing_handler(e):
348 raise ValueError("x")
350 bus.subscribe("t", failing_handler, handler_name="h")
351 await bus.start()
352 for i in range(5):
353 await bus.publish(Event(topic="t"))
354 await asyncio.sleep(0.3)
355 await bus.stop()
357 dlq = bus.get_dlq()
358 assert len(dlq) == 2
360 @pytest.mark.asyncio
361 async def test_clear_dlq(self):
362 bus = EventBus(dlq_enabled=True)
364 async def failing_handler(e):
365 raise ValueError("x")
367 bus.subscribe("t", failing_handler, handler_name="h")
368 await bus.start()
369 await bus.publish(Event(topic="t"))
370 await asyncio.sleep(0.2)
371 await bus.stop()
373 assert len(bus.get_dlq()) == 1
374 cleared = bus.clear_dlq()
375 assert cleared == 1
376 assert len(bus.get_dlq()) == 0
378 @pytest.mark.asyncio
379 async def test_replay_dlq(self):
380 bus = EventBus(dlq_enabled=True)
382 async def failing_handler(e):
383 raise ValueError("x")
385 bus.subscribe("t", failing_handler, handler_name="h")
386 await bus.start()
387 await bus.publish(Event(topic="t"))
388 await asyncio.sleep(0.2)
389 await bus.stop()
391 assert len(bus.get_dlq()) == 1
393 # Restart bus and replay
394 await bus.start()
395 replayed = await bus.replay_dlq()
396 assert replayed == 1
397 assert len(bus.get_dlq()) == 0
398 await bus.stop()
401# ============================================================================
402# EventBus — Lifecycle
403# ============================================================================
405class TestEventBusLifecycle:
406 @pytest.mark.asyncio
407 async def test_start_idempotent(self):
408 bus = EventBus()
409 await bus.start()
410 await bus.start() # should not double-start
411 await bus.stop()
413 @pytest.mark.asyncio
414 async def test_stop_drains_queue(self):
415 bus = EventBus(worker_count=2)
416 received = []
418 async def handler(e):
419 received.append(e.payload)
420 await asyncio.sleep(0) # yield to event loop to allow processing
422 bus.subscribe("t", handler, handler_name="h")
423 await bus.start()
424 await bus.publish(Event(topic="t", payload="drain"))
425 await asyncio.sleep(0.3)
426 await bus.stop(grace_period=3.0)
427 assert "drain" in received
430# ============================================================================
431# Default bus
432# ============================================================================
434class TestDefaultBus:
435 def test_default_bus_exists(self):
436 assert isinstance(default_bus, EventBus)