Skip to content

Commit 8969033

Browse files
committed
test(sessions): add normalizer unit tests for replay consistency
15 tests covering event normalization (author, text, state_delta) and snapshot normalization (session_id, state, events, memories, summaries). Signed-off-by: coder-mtj <coder-mtj@users.noreply.github.com>
1 parent 099b571 commit 8969033

2 files changed

Lines changed: 291 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Tencent is pleased to support the open source community by making
2+
# tRPC-Agent-Python available.
3+
#
4+
# Copyright (C) 2026 Tencent. All rights reserved.
5+
#
6+
# tRPC-Agent-Python is licensed under Apache-2.0.
7+
8+
"""Normalizer for replay consistency testing.
9+
10+
Strips auto-generated fields from events and snapshots so that
11+
cross-backend comparisons are deterministic and meaningful.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from typing import Any
17+
18+
from trpc_agent_sdk.events import Event
19+
from trpc_agent_sdk.sessions._session import Session
20+
21+
22+
def normalize_event(event: Event) -> dict[str, Any]:
23+
"""Strip auto-generated fields from an event for comparison.
24+
25+
Args:
26+
event: The raw Event from a session service.
27+
28+
Returns:
29+
A dict with only the business-relevant fields: author, text, and
30+
optionally state_delta.
31+
"""
32+
norm: dict[str, Any] = {
33+
"author": event.author,
34+
}
35+
36+
# Extract text from content parts.
37+
if event.content and event.content.parts:
38+
texts: list[str] = []
39+
for part in event.content.parts:
40+
if hasattr(part, "text") and part.text:
41+
texts.append(part.text)
42+
norm["text"] = " ".join(texts)
43+
else:
44+
norm["text"] = ""
45+
46+
# Include state_delta if present.
47+
if event.actions and event.actions.state_delta:
48+
norm["state_delta"] = {
49+
k: v for k, v in event.actions.state_delta.items()
50+
}
51+
52+
return norm
53+
54+
55+
def normalize_snapshot(
56+
session: Session,
57+
memories: list[dict[str, Any]],
58+
) -> dict[str, Any]:
59+
"""Produce a normalized snapshot for cross-backend comparison.
60+
61+
Args:
62+
session: The Session object after replay.
63+
memories: List of memory dicts with at least "content" key.
64+
65+
Returns:
66+
A dict with keys: session_id, state, events, memories, summaries.
67+
"""
68+
events_norm = [normalize_event(e) for e in session.events]
69+
70+
return {
71+
"session_id": session.id,
72+
"state": dict(session.state) if session.state else {},
73+
"events": events_norm,
74+
"memories": sorted(memories, key=lambda m: m.get("content", "")),
75+
"summaries": (
76+
dict(session.summaries)
77+
if hasattr(session, "summaries") and session.summaries
78+
else {}
79+
),
80+
}
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# Tencent is pleased to support the open source community by making
2+
# tRPC-Agent-Python available.
3+
#
4+
# Copyright (C) 2026 Tencent. All rights reserved.
5+
#
6+
# tRPC-Agent-Python is licensed under Apache-2.0.
7+
8+
"""Unit tests for the replay consistency normalizer."""
9+
10+
from __future__ import annotations
11+
12+
import pytest
13+
14+
from trpc_agent_sdk.events import Event
15+
from trpc_agent_sdk.sessions._session import Session
16+
from trpc_agent_sdk.types import Content, EventActions, Part
17+
18+
19+
def _make_event(
20+
author: str,
21+
text: str = "",
22+
invocation_id: str = "inv-001",
23+
state_delta: dict[str, str] | None = None,
24+
) -> Event:
25+
actions = EventActions(state_delta=state_delta) if state_delta else EventActions()
26+
content = Content(parts=[Part.from_text(text=text)] if text else [])
27+
return Event(
28+
invocation_id=invocation_id,
29+
author=author,
30+
content=content,
31+
actions=actions,
32+
)
33+
34+
35+
# ---------------------------------------------------------------------------
36+
# RED phase — these tests fail until normalizer is implemented
37+
# ---------------------------------------------------------------------------
38+
39+
40+
class TestNormalizeEvent:
41+
"""Tests for normalize_event()."""
42+
43+
def test_preserves_author(self):
44+
from tests.sessions.replay_consistency.normalizer import normalize_event
45+
event = _make_event(author="user", text="Hello")
46+
norm = normalize_event(event)
47+
assert norm["author"] == "user"
48+
49+
def test_extracts_text_from_parts(self):
50+
from tests.sessions.replay_consistency.normalizer import normalize_event
51+
content = Content(parts=[
52+
Part.from_text(text="Hello"),
53+
Part.from_text(text="world!"),
54+
])
55+
event = Event(
56+
invocation_id="inv-001",
57+
author="assistant",
58+
content=content,
59+
actions=EventActions(),
60+
)
61+
norm = normalize_event(event)
62+
assert norm["text"] == "Hello world!"
63+
64+
def test_handles_empty_parts(self):
65+
from tests.sessions.replay_consistency.normalizer import normalize_event
66+
event = Event(
67+
invocation_id="inv-001",
68+
author="user",
69+
content=Content(parts=[]),
70+
actions=EventActions(),
71+
)
72+
norm = normalize_event(event)
73+
assert norm.get("text", "") == ""
74+
75+
def test_handles_content_without_text(self):
76+
from tests.sessions.replay_consistency.normalizer import normalize_event
77+
content = Content(parts=[Part.from_text(text="")])
78+
event = Event(
79+
invocation_id="inv-001",
80+
author="assistant",
81+
content=content,
82+
actions=EventActions(),
83+
)
84+
norm = normalize_event(event)
85+
assert "text" in norm
86+
assert norm["text"] == ""
87+
88+
def test_includes_state_delta(self):
89+
from tests.sessions.replay_consistency.normalizer import normalize_event
90+
event = _make_event(
91+
author="assistant",
92+
text="Updated.",
93+
state_delta={"user:score": "10"},
94+
)
95+
norm = normalize_event(event)
96+
assert "state_delta" in norm
97+
assert norm["state_delta"] == {"user:score": "10"}
98+
99+
def test_no_state_delta_when_absent(self):
100+
from tests.sessions.replay_consistency.normalizer import normalize_event
101+
event = _make_event(author="user", text="Hi")
102+
norm = normalize_event(event)
103+
assert "state_delta" not in norm
104+
105+
def test_multi_part_tool_call_event(self):
106+
from tests.sessions.replay_consistency.normalizer import normalize_event
107+
content = Content(parts=[Part.from_text(text="Calling tool...")])
108+
event = Event(
109+
invocation_id="inv-002",
110+
author="assistant",
111+
content=content,
112+
actions=EventActions(),
113+
)
114+
norm = normalize_event(event)
115+
assert norm["author"] == "assistant"
116+
assert "Calling tool..." in norm["text"]
117+
118+
119+
class TestNormalizeSnapshot:
120+
"""Tests for normalize_snapshot()."""
121+
122+
def test_includes_session_id(self):
123+
from tests.sessions.replay_consistency.normalizer import normalize_snapshot
124+
session = Session(
125+
id="session-001", app_name="test-app", user_id="user-1",
126+
state={}, events=[], save_key="test-key",
127+
)
128+
snap = normalize_snapshot(session, [])
129+
assert snap["session_id"] == "session-001"
130+
131+
def test_includes_state_as_dict(self):
132+
from tests.sessions.replay_consistency.normalizer import normalize_snapshot
133+
session = Session(
134+
id="session-001", app_name="test-app", user_id="user-1",
135+
state={"key": "value"}, events=[], save_key="test-key",
136+
)
137+
snap = normalize_snapshot(session, [])
138+
assert snap["state"] == {"key": "value"}
139+
140+
def test_empty_state_becomes_empty_dict(self):
141+
from tests.sessions.replay_consistency.normalizer import normalize_snapshot
142+
session = Session(
143+
id="session-001", app_name="test-app", user_id="user-1",
144+
state={}, events=[], save_key="test-key",
145+
)
146+
snap = normalize_snapshot(session, [])
147+
assert snap["state"] == {}
148+
149+
def test_normalizes_multiple_events(self):
150+
from tests.sessions.replay_consistency.normalizer import normalize_snapshot
151+
events = [
152+
_make_event(author="user", text="Q1", invocation_id="inv-1"),
153+
_make_event(author="assistant", text="A1", invocation_id="inv-2"),
154+
_make_event(author="user", text="Q2", invocation_id="inv-3"),
155+
]
156+
session = Session(
157+
id="session-002", app_name="test-app", user_id="user-1",
158+
state={}, events=events, save_key="test-key",
159+
)
160+
snap = normalize_snapshot(session, [])
161+
assert len(snap["events"]) == 3
162+
assert snap["events"][0]["author"] == "user"
163+
assert snap["events"][0]["text"] == "Q1"
164+
assert snap["events"][1]["text"] == "A1"
165+
166+
def test_sorts_memories_by_content(self):
167+
from tests.sessions.replay_consistency.normalizer import normalize_snapshot
168+
session = Session(
169+
id="session-003", app_name="test-app", user_id="user-1",
170+
state={}, events=[], save_key="test-key",
171+
)
172+
memories = [
173+
{"content": "Zebra", "topics": ["animals"]},
174+
{"content": "Apple", "topics": ["fruit"]},
175+
{"content": "Mango", "topics": ["fruit"]},
176+
]
177+
snap = normalize_snapshot(session, memories)
178+
sorted_contents = [m["content"] for m in snap["memories"]]
179+
assert sorted_contents == ["Apple", "Mango", "Zebra"]
180+
181+
def test_includes_summaries(self):
182+
from tests.sessions.replay_consistency.normalizer import normalize_snapshot
183+
session = Session(
184+
id="session-004", app_name="test-app", user_id="user-1",
185+
state={}, events=[], save_key="test-key",
186+
)
187+
snap = normalize_snapshot(session, [])
188+
summaries = snap.get("summaries", {})
189+
assert isinstance(summaries, dict)
190+
191+
def test_unicode_text_preserved(self):
192+
from tests.sessions.replay_consistency.normalizer import normalize_snapshot
193+
events = [
194+
_make_event(author="user", text="你好世界 \U0001f680测试", invocation_id="inv-1"),
195+
]
196+
session = Session(
197+
id="session-u", app_name="test-app", user_id="user-1",
198+
state={}, events=events, save_key="test-key",
199+
)
200+
snap = normalize_snapshot(session, [])
201+
assert "你好世界 \U0001f680测试" in snap["events"][0]["text"]
202+
203+
def test_empty_session_events(self):
204+
from tests.sessions.replay_consistency.normalizer import normalize_snapshot
205+
session = Session(
206+
id="session-empty", app_name="test-app", user_id="user-1",
207+
state={}, events=[], save_key="test-key",
208+
)
209+
snap = normalize_snapshot(session, [])
210+
assert snap["events"] == []
211+
assert snap["memories"] == []

0 commit comments

Comments
 (0)