667 lines
22 KiB
Python
667 lines
22 KiB
Python
"""Tests for EventQueue — SQ/EQ 双队列实现
|
||
|
||
测试场景:
|
||
- SQ 正确接收用户输入并返回 task_id
|
||
- EQ 正确推送事件给订阅者
|
||
- 多订阅者同时接收事件(广播)
|
||
- 事件缓冲对新订阅者的回放
|
||
- SQ 取消任务
|
||
- 事件类型正确分类
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
from datetime import datetime
|
||
|
||
from agentkit.core.event_queue import EventQueue, Submission, SubmissionQueue
|
||
from agentkit.core.protocol import (
|
||
Event,
|
||
SessionEventType,
|
||
TaskEventType,
|
||
TurnEventType,
|
||
)
|
||
|
||
|
||
# ── SubmissionQueue Tests ───────────────────────────────────────
|
||
|
||
|
||
class TestSubmissionQueue:
|
||
"""SubmissionQueue 单元测试"""
|
||
|
||
async def test_submit_returns_task_id(self):
|
||
"""测试 submit 返回有效的 task_id"""
|
||
sq = SubmissionQueue()
|
||
task_id = await sq.submit("hello", "session-1")
|
||
|
||
assert isinstance(task_id, str)
|
||
assert len(task_id) > 0
|
||
|
||
async def test_submit_returns_unique_task_ids(self):
|
||
"""测试每次 submit 返回不同的 task_id"""
|
||
sq = SubmissionQueue()
|
||
task_id_1 = await sq.submit("hello", "session-1")
|
||
task_id_2 = await sq.submit("world", "session-1")
|
||
|
||
assert task_id_1 != task_id_2
|
||
|
||
async def test_submit_stores_submission(self):
|
||
"""测试 submit 正确存储提交内容"""
|
||
sq = SubmissionQueue()
|
||
task_id = await sq.submit("hello world", "session-1")
|
||
|
||
assert task_id in sq._submissions
|
||
submission = sq._submissions[task_id]
|
||
assert submission.content == "hello world"
|
||
assert submission.session_id == "session-1"
|
||
assert submission.task_id == task_id
|
||
assert submission.cancelled is False
|
||
|
||
async def test_drain_receives_submissions_in_order(self):
|
||
"""测试 drain 按提交顺序接收提交"""
|
||
sq = SubmissionQueue()
|
||
await sq.submit("first", "session-1")
|
||
await sq.submit("second", "session-1")
|
||
|
||
received: list[str] = []
|
||
|
||
async def consumer():
|
||
async for submission in sq.drain():
|
||
received.append(submission.content)
|
||
if len(received) >= 2:
|
||
break
|
||
|
||
consumer_task = asyncio.create_task(consumer())
|
||
await asyncio.wait_for(consumer_task, timeout=1.0)
|
||
|
||
assert received == ["first", "second"]
|
||
|
||
async def test_drain_preserves_submission_fields(self):
|
||
"""测试 drain 返回的 Submission 字段完整"""
|
||
sq = SubmissionQueue()
|
||
await sq.submit("hello", "session-1")
|
||
|
||
received: list[Submission] = []
|
||
|
||
async def consumer():
|
||
async for submission in sq.drain():
|
||
received.append(submission)
|
||
break
|
||
|
||
consumer_task = asyncio.create_task(consumer())
|
||
await asyncio.wait_for(consumer_task, timeout=1.0)
|
||
|
||
assert len(received) == 1
|
||
sub = received[0]
|
||
assert sub.content == "hello"
|
||
assert sub.session_id == "session-1"
|
||
assert isinstance(sub.task_id, str)
|
||
assert isinstance(sub.created_at, datetime)
|
||
|
||
async def test_cancel_task_succeeds(self):
|
||
"""测试取消已存在的任务"""
|
||
sq = SubmissionQueue()
|
||
task_id = await sq.submit("hello", "session-1")
|
||
|
||
result = await sq.cancel(task_id)
|
||
|
||
assert result is True
|
||
assert task_id in sq._cancelled_tasks
|
||
assert sq._submissions[task_id].cancelled is True
|
||
|
||
async def test_cancel_nonexistent_task_returns_false(self):
|
||
"""测试取消不存在的任务返回 False"""
|
||
sq = SubmissionQueue()
|
||
result = await sq.cancel("nonexistent-task-id")
|
||
|
||
assert result is False
|
||
|
||
async def test_cancel_already_cancelled_task_returns_false(self):
|
||
"""测试重复取消返回 False"""
|
||
sq = SubmissionQueue()
|
||
task_id = await sq.submit("hello", "session-1")
|
||
|
||
first_cancel = await sq.cancel(task_id)
|
||
second_cancel = await sq.cancel(task_id)
|
||
|
||
assert first_cancel is True
|
||
assert second_cancel is False
|
||
|
||
async def test_drain_skips_cancelled_submissions(self):
|
||
"""测试 drain 跳过已取消的提交"""
|
||
sq = SubmissionQueue()
|
||
task_id_1 = await sq.submit("first", "session-1")
|
||
await sq.submit("second", "session-1")
|
||
|
||
# 取消第一个提交
|
||
await sq.cancel(task_id_1)
|
||
|
||
received: list[str] = []
|
||
|
||
async def consumer():
|
||
async for submission in sq.drain():
|
||
received.append(submission.content)
|
||
if len(received) >= 1:
|
||
break
|
||
|
||
consumer_task = asyncio.create_task(consumer())
|
||
await asyncio.wait_for(consumer_task, timeout=1.0)
|
||
|
||
# 应只收到第二个提交
|
||
assert received == ["second"]
|
||
|
||
async def test_close_prevents_new_submissions(self):
|
||
"""测试关闭后不再接受新提交"""
|
||
sq = SubmissionQueue()
|
||
sq.close()
|
||
|
||
assert sq.is_closed is True
|
||
|
||
try:
|
||
await sq.submit("hello", "session-1")
|
||
raise AssertionError("Should have raised RuntimeError")
|
||
except RuntimeError:
|
||
pass
|
||
|
||
async def test_close_does_not_affect_existing_submissions(self):
|
||
"""测试关闭后已提交的内容仍可消费"""
|
||
sq = SubmissionQueue()
|
||
await sq.submit("before-close", "session-1")
|
||
sq.close()
|
||
|
||
received: list[str] = []
|
||
|
||
async def consumer():
|
||
async for submission in sq.drain():
|
||
received.append(submission.content)
|
||
break
|
||
|
||
consumer_task = asyncio.create_task(consumer())
|
||
await asyncio.wait_for(consumer_task, timeout=1.0)
|
||
|
||
assert received == ["before-close"]
|
||
|
||
|
||
# ── EventQueue Tests ────────────────────────────────────────────
|
||
|
||
|
||
class TestEventQueue:
|
||
"""EventQueue 单元测试"""
|
||
|
||
async def test_emit_and_subscribe_single_event(self):
|
||
"""测试 EQ 正确推送事件给订阅者"""
|
||
eq = EventQueue()
|
||
event = Event.create(
|
||
event_type=TurnEventType.TOKEN,
|
||
task_id="task-1",
|
||
session_id="session-1",
|
||
data={"text": "hello"},
|
||
)
|
||
|
||
received: list[Event] = []
|
||
|
||
async def subscriber():
|
||
async for evt in eq.subscribe():
|
||
received.append(evt)
|
||
break
|
||
|
||
sub_task = asyncio.create_task(subscriber())
|
||
await asyncio.sleep(0.05) # 给订阅者启动时间
|
||
|
||
await eq.emit(event)
|
||
await asyncio.wait_for(sub_task, timeout=1.0)
|
||
|
||
assert len(received) == 1
|
||
assert received[0].event_type == TurnEventType.TOKEN
|
||
assert received[0].task_id == "task-1"
|
||
assert received[0].session_id == "session-1"
|
||
assert received[0].data == {"text": "hello"}
|
||
|
||
async def test_emit_preserves_event_fields(self):
|
||
"""测试 emit 不修改事件字段"""
|
||
eq = EventQueue()
|
||
original = Event.create(
|
||
event_type=TaskEventType.TASK_STARTED,
|
||
task_id="task-1",
|
||
session_id="session-1",
|
||
data={"agent": "react"},
|
||
)
|
||
|
||
received: list[Event] = []
|
||
|
||
async def subscriber():
|
||
async for evt in eq.subscribe():
|
||
received.append(evt)
|
||
break
|
||
|
||
sub_task = asyncio.create_task(subscriber())
|
||
await asyncio.sleep(0.05)
|
||
|
||
await eq.emit(original)
|
||
await asyncio.wait_for(sub_task, timeout=1.0)
|
||
|
||
assert received[0] is original or received[0].to_dict() == original.to_dict()
|
||
|
||
async def test_broadcast_to_multiple_subscribers(self):
|
||
"""测试多订阅者同时接收事件(广播)"""
|
||
eq = EventQueue()
|
||
|
||
received_a: list[Event] = []
|
||
received_b: list[Event] = []
|
||
|
||
async def subscriber_a():
|
||
async for evt in eq.subscribe():
|
||
received_a.append(evt)
|
||
if len(received_a) >= 2:
|
||
break
|
||
|
||
async def subscriber_b():
|
||
async for evt in eq.subscribe():
|
||
received_b.append(evt)
|
||
if len(received_b) >= 2:
|
||
break
|
||
|
||
task_a = asyncio.create_task(subscriber_a())
|
||
task_b = asyncio.create_task(subscriber_b())
|
||
await asyncio.sleep(0.05) # 给订阅者启动时间
|
||
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 1}))
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 2}))
|
||
|
||
await asyncio.wait_for(task_a, timeout=1.0)
|
||
await asyncio.wait_for(task_b, timeout=1.0)
|
||
|
||
assert len(received_a) == 2
|
||
assert len(received_b) == 2
|
||
assert received_a[0].data == {"seq": 1}
|
||
assert received_a[1].data == {"seq": 2}
|
||
assert received_b[0].data == {"seq": 1}
|
||
assert received_b[1].data == {"seq": 2}
|
||
|
||
async def test_buffer_replay_for_new_subscriber(self):
|
||
"""测试事件缓冲对新订阅者的回放"""
|
||
eq = EventQueue(buffer_size=100)
|
||
|
||
# 先发送几条事件(无订阅者)
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 1}))
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 2}))
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 3}))
|
||
|
||
# 新订阅者应收到缓冲回放
|
||
received: list[Event] = []
|
||
|
||
async def subscriber():
|
||
async for evt in eq.subscribe():
|
||
received.append(evt)
|
||
if len(received) >= 3:
|
||
break
|
||
|
||
sub_task = asyncio.create_task(subscriber())
|
||
await asyncio.wait_for(sub_task, timeout=1.0)
|
||
|
||
assert len(received) == 3
|
||
assert received[0].data == {"seq": 1}
|
||
assert received[1].data == {"seq": 2}
|
||
assert received[2].data == {"seq": 3}
|
||
|
||
async def test_buffer_replay_then_live_events(self):
|
||
"""测试新订阅者先收到回放,再收到新事件"""
|
||
eq = EventQueue(buffer_size=100)
|
||
|
||
# 先发送 2 条事件(进入缓冲)
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 1}))
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 2}))
|
||
|
||
received: list[Event] = []
|
||
|
||
async def subscriber():
|
||
async for evt in eq.subscribe():
|
||
received.append(evt)
|
||
if len(received) >= 4:
|
||
break
|
||
|
||
sub_task = asyncio.create_task(subscriber())
|
||
await asyncio.sleep(0.05) # 给订阅者启动时间(回放缓冲)
|
||
|
||
# 再发送 2 条新事件
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 3}))
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 4}))
|
||
|
||
await asyncio.wait_for(sub_task, timeout=1.0)
|
||
|
||
assert len(received) == 4
|
||
assert [r.data["seq"] for r in received] == [1, 2, 3, 4]
|
||
|
||
async def test_buffer_size_limit_keeps_latest(self):
|
||
"""测试缓冲区大小限制,只保留最新 N 条"""
|
||
eq = EventQueue(buffer_size=3)
|
||
|
||
# 发送 5 条事件,缓冲区只保留最后 3 条
|
||
for i in range(5):
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": i}))
|
||
|
||
received: list[Event] = []
|
||
|
||
async def subscriber():
|
||
async for evt in eq.subscribe():
|
||
received.append(evt)
|
||
if len(received) >= 3:
|
||
break
|
||
|
||
sub_task = asyncio.create_task(subscriber())
|
||
await asyncio.wait_for(sub_task, timeout=1.0)
|
||
|
||
assert len(received) == 3
|
||
# 应该是最后 3 条(seq: 2, 3, 4)
|
||
assert [r.data["seq"] for r in received] == [2, 3, 4]
|
||
|
||
async def test_default_buffer_size_is_100(self):
|
||
"""测试默认缓冲区大小为 100"""
|
||
eq = EventQueue()
|
||
|
||
assert eq.buffer_size == 100
|
||
|
||
async def test_close_unblocks_subscribers(self):
|
||
"""测试 close 解除订阅者阻塞"""
|
||
eq = EventQueue()
|
||
|
||
async def subscriber():
|
||
async for _ in eq.subscribe():
|
||
pass # 消费事件直到队列关闭
|
||
|
||
sub_task = asyncio.create_task(subscriber())
|
||
await asyncio.sleep(0.05)
|
||
|
||
eq.close()
|
||
await asyncio.wait_for(sub_task, timeout=1.0)
|
||
|
||
assert sub_task.done()
|
||
assert eq.is_closed is True
|
||
|
||
async def test_subscribe_after_close_returns_immediately(self):
|
||
"""测试关闭后订阅立即返回(不阻塞)"""
|
||
eq = EventQueue()
|
||
eq.close()
|
||
|
||
received: list[Event] = []
|
||
|
||
async def subscriber():
|
||
async for evt in eq.subscribe():
|
||
received.append(evt)
|
||
|
||
sub_task = asyncio.create_task(subscriber())
|
||
await asyncio.wait_for(sub_task, timeout=1.0)
|
||
|
||
assert sub_task.done()
|
||
assert len(received) == 0
|
||
|
||
async def test_subscriber_count_tracks_subscriptions(self):
|
||
"""测试订阅者计数正确跟踪订阅"""
|
||
eq = EventQueue()
|
||
|
||
assert eq.subscriber_count == 0
|
||
|
||
async def subscriber():
|
||
async for _ in eq.subscribe():
|
||
pass
|
||
|
||
task = asyncio.create_task(subscriber())
|
||
await asyncio.sleep(0.05)
|
||
|
||
assert eq.subscriber_count == 1
|
||
|
||
eq.close()
|
||
await asyncio.wait_for(task, timeout=1.0)
|
||
|
||
assert eq.subscriber_count == 0
|
||
|
||
async def test_subscriber_removed_on_explicit_close(self):
|
||
"""测试显式关闭订阅生成器后从列表移除
|
||
|
||
注意:async for 的 break 不会立即触发生成器的 finally,
|
||
需要显式调用 aclose() 才能保证清理。
|
||
"""
|
||
eq = EventQueue()
|
||
|
||
received: list[Event] = []
|
||
|
||
async def subscriber():
|
||
gen = eq.subscribe()
|
||
try:
|
||
async for evt in gen:
|
||
received.append(evt)
|
||
break
|
||
finally:
|
||
await gen.aclose()
|
||
|
||
task = asyncio.create_task(subscriber())
|
||
await asyncio.sleep(0.05)
|
||
assert eq.subscriber_count == 1
|
||
|
||
# 触发一次 emit 让订阅者能收到事件并 break
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 1}))
|
||
await asyncio.wait_for(task, timeout=1.0)
|
||
|
||
assert len(received) == 1
|
||
assert eq.subscriber_count == 0
|
||
|
||
async def test_emit_to_no_subscribers_still_buffers(self):
|
||
"""测试无订阅者时 emit 仍写入缓冲区"""
|
||
eq = EventQueue(buffer_size=100)
|
||
|
||
await eq.emit(Event.create(TurnEventType.TOKEN, "task-1", "session-1", {"seq": 1}))
|
||
|
||
# 缓冲区应有 1 条
|
||
assert len(eq._buffer) == 1
|
||
|
||
# 新订阅者应收到回放
|
||
received: list[Event] = []
|
||
|
||
async def subscriber():
|
||
async for evt in eq.subscribe():
|
||
received.append(evt)
|
||
break
|
||
|
||
sub_task = asyncio.create_task(subscriber())
|
||
await asyncio.wait_for(sub_task, timeout=1.0)
|
||
|
||
assert len(received) == 1
|
||
assert received[0].data == {"seq": 1}
|
||
|
||
|
||
# ── Event Type Tests ────────────────────────────────────────────
|
||
|
||
|
||
class TestEventTypes:
|
||
"""事件类型分类测试"""
|
||
|
||
def test_session_event_types(self):
|
||
"""测试 Session 级别事件类型"""
|
||
assert SessionEventType.SESSION_STARTED == "session.started"
|
||
assert SessionEventType.SESSION_ENDED == "session.ended"
|
||
|
||
def test_task_event_types(self):
|
||
"""测试 Task 级别事件类型"""
|
||
assert TaskEventType.TASK_CREATED == "task.created"
|
||
assert TaskEventType.TASK_STARTED == "task.started"
|
||
assert TaskEventType.TASK_COMPLETED == "task.completed"
|
||
assert TaskEventType.TASK_FAILED == "task.failed"
|
||
|
||
def test_turn_event_types(self):
|
||
"""测试 Turn 级别事件类型"""
|
||
assert TurnEventType.TURN_STARTED == "turn.started"
|
||
assert TurnEventType.THINKING == "turn.thinking"
|
||
assert TurnEventType.TOOL_CALL == "turn.tool_call"
|
||
assert TurnEventType.TOOL_RESULT == "turn.tool_result"
|
||
assert TurnEventType.TOKEN == "turn.token"
|
||
assert TurnEventType.STEP == "turn.step"
|
||
assert TurnEventType.FINAL_ANSWER == "turn.final_answer"
|
||
assert TurnEventType.TURN_COMPLETED == "turn.completed"
|
||
|
||
def test_event_type_prefixes(self):
|
||
"""测试事件类型按前缀正确分类"""
|
||
session_events = [SessionEventType.SESSION_STARTED, SessionEventType.SESSION_ENDED]
|
||
task_events = [
|
||
TaskEventType.TASK_CREATED,
|
||
TaskEventType.TASK_STARTED,
|
||
TaskEventType.TASK_COMPLETED,
|
||
TaskEventType.TASK_FAILED,
|
||
]
|
||
turn_events = [
|
||
TurnEventType.TURN_STARTED,
|
||
TurnEventType.THINKING,
|
||
TurnEventType.TOOL_CALL,
|
||
TurnEventType.TOOL_RESULT,
|
||
TurnEventType.TOKEN,
|
||
TurnEventType.STEP,
|
||
TurnEventType.FINAL_ANSWER,
|
||
TurnEventType.TURN_COMPLETED,
|
||
]
|
||
|
||
for evt in session_events:
|
||
assert evt.startswith("session."), f"{evt} should start with 'session.'"
|
||
for evt in task_events:
|
||
assert evt.startswith("task."), f"{evt} should start with 'task.'"
|
||
for evt in turn_events:
|
||
assert evt.startswith("turn."), f"{evt} should start with 'turn.'"
|
||
|
||
def test_event_types_are_distinct(self):
|
||
"""测试所有事件类型互不相同"""
|
||
all_types = [
|
||
SessionEventType.SESSION_STARTED,
|
||
SessionEventType.SESSION_ENDED,
|
||
TaskEventType.TASK_CREATED,
|
||
TaskEventType.TASK_STARTED,
|
||
TaskEventType.TASK_COMPLETED,
|
||
TaskEventType.TASK_FAILED,
|
||
TurnEventType.TURN_STARTED,
|
||
TurnEventType.THINKING,
|
||
TurnEventType.TOOL_CALL,
|
||
TurnEventType.TOOL_RESULT,
|
||
TurnEventType.TOKEN,
|
||
TurnEventType.STEP,
|
||
TurnEventType.FINAL_ANSWER,
|
||
TurnEventType.TURN_COMPLETED,
|
||
]
|
||
|
||
assert len(all_types) == len(set(all_types)), "Event types should be distinct"
|
||
|
||
|
||
# ── Event Dataclass Tests ───────────────────────────────────────
|
||
|
||
|
||
class TestEventDataclass:
|
||
"""Event 数据结构测试"""
|
||
|
||
def test_event_creation(self):
|
||
"""测试 Event 创建"""
|
||
event = Event(
|
||
event_type=TurnEventType.TOKEN,
|
||
task_id="task-1",
|
||
session_id="session-1",
|
||
data={"text": "hello"},
|
||
timestamp="2025-01-01T00:00:00+00:00",
|
||
)
|
||
|
||
assert event.event_type == TurnEventType.TOKEN
|
||
assert event.task_id == "task-1"
|
||
assert event.session_id == "session-1"
|
||
assert event.data == {"text": "hello"}
|
||
assert event.timestamp == "2025-01-01T00:00:00+00:00"
|
||
|
||
def test_event_create_factory_generates_timestamp(self):
|
||
"""测试 Event.create 工厂方法自动生成时间戳"""
|
||
event = Event.create(
|
||
event_type=TaskEventType.TASK_STARTED,
|
||
task_id="task-1",
|
||
session_id="session-1",
|
||
data={"agent": "react"},
|
||
)
|
||
|
||
assert event.event_type == TaskEventType.TASK_STARTED
|
||
assert event.task_id == "task-1"
|
||
assert event.session_id == "session-1"
|
||
assert event.data == {"agent": "react"}
|
||
assert len(event.timestamp) > 0
|
||
# 时间戳应为 ISO 8601 格式(fromisoformat 不抛异常即正确)
|
||
datetime.fromisoformat(event.timestamp)
|
||
|
||
def test_event_create_with_default_data(self):
|
||
"""测试 Event.create 不传 data 时默认为空 dict"""
|
||
event = Event.create(
|
||
event_type=SessionEventType.SESSION_STARTED,
|
||
task_id="task-1",
|
||
session_id="session-1",
|
||
)
|
||
|
||
assert event.data == {}
|
||
|
||
def test_event_to_dict(self):
|
||
"""测试 Event.to_dict"""
|
||
event = Event(
|
||
event_type=TurnEventType.TOKEN,
|
||
task_id="task-1",
|
||
session_id="session-1",
|
||
data={"text": "hello"},
|
||
timestamp="2025-01-01T00:00:00+00:00",
|
||
)
|
||
|
||
d = event.to_dict()
|
||
|
||
assert d == {
|
||
"event_type": TurnEventType.TOKEN,
|
||
"task_id": "task-1",
|
||
"session_id": "session-1",
|
||
"data": {"text": "hello"},
|
||
"timestamp": "2025-01-01T00:00:00+00:00",
|
||
}
|
||
|
||
def test_event_from_dict(self):
|
||
"""测试 Event.from_dict"""
|
||
data = {
|
||
"event_type": TurnEventType.TOKEN,
|
||
"task_id": "task-1",
|
||
"session_id": "session-1",
|
||
"data": {"text": "hello"},
|
||
"timestamp": "2025-01-01T00:00:00+00:00",
|
||
}
|
||
|
||
event = Event.from_dict(data)
|
||
|
||
assert event.event_type == TurnEventType.TOKEN
|
||
assert event.task_id == "task-1"
|
||
assert event.session_id == "session-1"
|
||
assert event.data == {"text": "hello"}
|
||
assert event.timestamp == "2025-01-01T00:00:00+00:00"
|
||
|
||
def test_event_to_dict_from_dict_roundtrip(self):
|
||
"""测试 to_dict -> from_dict 往返保持一致"""
|
||
original = Event.create(
|
||
event_type=SessionEventType.SESSION_STARTED,
|
||
task_id="task-1",
|
||
session_id="session-1",
|
||
data={"user": "alice"},
|
||
)
|
||
|
||
d = original.to_dict()
|
||
restored = Event.from_dict(d)
|
||
|
||
assert restored.event_type == original.event_type
|
||
assert restored.task_id == original.task_id
|
||
assert restored.session_id == original.session_id
|
||
assert restored.data == original.data
|
||
assert restored.timestamp == original.timestamp
|
||
|
||
def test_event_from_dict_with_missing_data_defaults_to_empty(self):
|
||
"""测试 from_dict 缺少 data 字段时默认为空 dict"""
|
||
data = {
|
||
"event_type": TurnEventType.TOKEN,
|
||
"task_id": "task-1",
|
||
"session_id": "session-1",
|
||
"timestamp": "2025-01-01T00:00:00+00:00",
|
||
}
|
||
|
||
event = Event.from_dict(data)
|
||
|
||
assert event.data == {}
|