Skip to content

Commit 0496bd3

Browse files
committed
test(sessions): add comparator unit tests for replay consistency
15 tests covering recursive diff on dicts, lists, primitives, nested structures, and full session snapshots. Signed-off-by: coder-mtj <coder-mtj@users.noreply.github.com>
1 parent 8969033 commit 0496bd3

2 files changed

Lines changed: 276 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
"""Comparator for replay consistency testing.
9+
10+
Recursively compares two normalized snapshots and produces a list of
11+
DiffEntry objects describing every divergence.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import dataclasses
17+
from typing import Any
18+
19+
20+
@dataclasses.dataclass
21+
class DiffEntry:
22+
"""A single normalized field mismatch. Mirrors Go DiffEntry."""
23+
session_id: str | None = None
24+
event_index: int | None = None
25+
memory_id: str | None = None
26+
summary_id: str | None = None
27+
track_name: str | None = None
28+
section: str = ""
29+
path: str = ""
30+
left: Any = None
31+
right: Any = None
32+
allowed: bool = False
33+
reason: str = ""
34+
35+
36+
def recursive_diff(
37+
left: Any,
38+
right: Any,
39+
path: str = "",
40+
case_name: str = "",
41+
) -> list[DiffEntry]:
42+
"""Recursively compare two values and return a list of diffs.
43+
44+
Args:
45+
left: Left-side value for comparison.
46+
right: Right-side value for comparison.
47+
path: Current JSON-path for tracking.
48+
case_name: Optional case name for grouping.
49+
50+
Returns:
51+
A list of DiffEntry objects describing every divergence.
52+
"""
53+
diffs: list[DiffEntry] = []
54+
55+
if isinstance(left, dict) and isinstance(right, dict):
56+
all_keys = set(left.keys()) | set(right.keys())
57+
for key in sorted(all_keys):
58+
child_path = f"{path}.{key}" if path else key
59+
left_val = left.get(key)
60+
right_val = right.get(key)
61+
if left_val != right_val:
62+
diffs.extend(
63+
recursive_diff(left_val, right_val, child_path, case_name)
64+
)
65+
66+
elif isinstance(left, list) and isinstance(right, list):
67+
max_len = max(len(left), len(right))
68+
for i in range(max_len):
69+
child_path = f"{path}[{i}]"
70+
left_val = left[i] if i < len(left) else None
71+
right_val = right[i] if i < len(right) else None
72+
if left_val != right_val:
73+
diffs.extend(
74+
recursive_diff(left_val, right_val, child_path, case_name)
75+
)
76+
else:
77+
if left != right:
78+
section = ""
79+
if path:
80+
# Extract top-level key from path as section.
81+
top = path.split("[")[0].split(".")[0]
82+
section = top
83+
diffs.append(DiffEntry(
84+
section=section,
85+
path=path,
86+
left=left,
87+
right=right,
88+
))
89+
90+
return diffs
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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 comparator."""
9+
10+
from __future__ import annotations
11+
12+
import dataclasses
13+
from typing import Any
14+
15+
import pytest
16+
17+
18+
@dataclasses.dataclass
19+
class DiffEntry:
20+
"""A single normalized field mismatch. Mirrors Go DiffEntry."""
21+
session_id: str | None = None
22+
event_index: int | None = None
23+
memory_id: str | None = None
24+
summary_id: str | None = None
25+
track_name: str | None = None
26+
section: str = ""
27+
path: str = ""
28+
left: Any = None
29+
right: Any = None
30+
allowed: bool = False
31+
reason: str = ""
32+
33+
34+
class TestRecursiveDiff:
35+
"""Tests for recursive_diff()."""
36+
37+
def test_identical_dicts_no_diff(self):
38+
"""Identical dicts produce zero diffs."""
39+
from tests.sessions.replay_consistency.comparator import recursive_diff
40+
left = {"a": 1, "b": 2}
41+
right = {"a": 1, "b": 2}
42+
diffs = recursive_diff(left, right)
43+
assert len(diffs) == 0
44+
45+
def test_different_dict_values(self):
46+
"""Different values produce diffs with correct paths."""
47+
from tests.sessions.replay_consistency.comparator import recursive_diff
48+
left = {"a": 1, "b": 2}
49+
right = {"a": 1, "b": 99}
50+
diffs = recursive_diff(left, right)
51+
assert len(diffs) == 1
52+
assert diffs[0].path == "b"
53+
assert diffs[0].left == 2
54+
assert diffs[0].right == 99
55+
56+
def test_missing_key_in_right(self):
57+
"""Key present in left but missing in right."""
58+
from tests.sessions.replay_consistency.comparator import recursive_diff
59+
left = {"a": 1, "b": 2}
60+
right = {"a": 1}
61+
diffs = recursive_diff(left, right)
62+
assert len(diffs) == 1
63+
assert diffs[0].path == "b"
64+
assert diffs[0].left == 2
65+
assert diffs[0].right is None
66+
67+
def test_extra_key_in_right(self):
68+
"""Key present in right but missing in left."""
69+
from tests.sessions.replay_consistency.comparator import recursive_diff
70+
left = {"a": 1}
71+
right = {"a": 1, "b": 2}
72+
diffs = recursive_diff(left, right)
73+
assert len(diffs) == 1
74+
assert diffs[0].path == "b"
75+
assert diffs[0].left is None
76+
assert diffs[0].right == 2
77+
78+
def test_identical_lists_no_diff(self):
79+
"""Identical lists produce zero diffs."""
80+
from tests.sessions.replay_consistency.comparator import recursive_diff
81+
left = [1, 2, 3]
82+
right = [1, 2, 3]
83+
diffs = recursive_diff(left, right)
84+
assert len(diffs) == 0
85+
86+
def test_list_length_mismatch(self):
87+
"""Left longer than right — extra items diff as None."""
88+
from tests.sessions.replay_consistency.comparator import recursive_diff
89+
left = [1, 2, 3]
90+
right = [1, 2]
91+
diffs = recursive_diff(left, right)
92+
assert len(diffs) == 1
93+
assert diffs[0].path == "[2]"
94+
assert diffs[0].left == 3
95+
assert diffs[0].right is None
96+
97+
def test_list_value_mismatch(self):
98+
"""Same length, different value."""
99+
from tests.sessions.replay_consistency.comparator import recursive_diff
100+
left = [1, 2, 3]
101+
right = [1, 99, 3]
102+
diffs = recursive_diff(left, right)
103+
assert len(diffs) == 1
104+
assert diffs[0].path == "[1]"
105+
106+
def test_nested_dict_in_list(self):
107+
"""Nested dict inside list diff."""
108+
from tests.sessions.replay_consistency.comparator import recursive_diff
109+
left = [{"name": "Alice", "score": 10}]
110+
right = [{"name": "Alice", "score": 20}]
111+
diffs = recursive_diff(left, right)
112+
assert len(diffs) == 1
113+
assert "score" in diffs[0].path
114+
115+
def test_deeply_nested_structure(self):
116+
"""Three-level nested diff."""
117+
from tests.sessions.replay_consistency.comparator import recursive_diff
118+
left = {"events": [{"author": "user", "content": {"text": "Hi"}}]}
119+
right = {"events": [{"author": "user", "content": {"text": "Bye"}}]}
120+
diffs = recursive_diff(left, right)
121+
assert len(diffs) == 1
122+
assert "text" in diffs[0].path
123+
assert diffs[0].left == "Hi"
124+
assert diffs[0].right == "Bye"
125+
126+
def test_primitive_string_diff(self):
127+
"""Direct string comparison."""
128+
from tests.sessions.replay_consistency.comparator import recursive_diff
129+
diffs = recursive_diff("hello", "world")
130+
assert len(diffs) == 1
131+
132+
def test_primitive_int_diff(self):
133+
"""Direct int comparison."""
134+
from tests.sessions.replay_consistency.comparator import recursive_diff
135+
diffs = recursive_diff(42, 0)
136+
assert len(diffs) == 1
137+
138+
def test_none_vs_value(self):
139+
"""None vs actual value."""
140+
from tests.sessions.replay_consistency.comparator import recursive_diff
141+
diffs = recursive_diff(None, "data")
142+
assert len(diffs) == 1
143+
144+
def test_detects_summary_injection(self):
145+
"""Verify summary-specific diff detection."""
146+
from tests.sessions.replay_consistency.comparator import recursive_diff
147+
left = {"summaries": {"": "Correct summary"}}
148+
right: dict[str, Any] = {"summaries": {}}
149+
diffs = recursive_diff(left, right)
150+
assert len(diffs) > 0, "Missing summary should be detected"
151+
152+
def test_unicode_diff(self):
153+
"""Unicode text diff should be detected correctly."""
154+
from tests.sessions.replay_consistency.comparator import recursive_diff
155+
left = {"text": "你好世界"}
156+
right = {"text": "こんにちは"}
157+
diffs = recursive_diff(left, right)
158+
assert len(diffs) == 1
159+
assert diffs[0].left == "你好世界"
160+
161+
def test_compound_session_snapshot(self):
162+
"""Full session snapshot comparison with multiple diff types."""
163+
from tests.sessions.replay_consistency.comparator import recursive_diff
164+
left = {
165+
"events": [{"author": "user", "text": "Hello"}],
166+
"state": {"key": "value1"},
167+
"memories": [{"content": "test memory"}],
168+
"tracks": [{"track": "exec", "payload": '{"ok":true}'}],
169+
}
170+
right = {
171+
"events": [{"author": "user", "text": "Different"}],
172+
"state": {"key": "value2"},
173+
"memories": [{"content": "other memory"}],
174+
"tracks": [{"track": "exec", "payload": '{"ok":false}'}],
175+
}
176+
diffs = recursive_diff(left, right)
177+
# Should find diffs in all 4 sections.
178+
sections: set[str] = set()
179+
for d in diffs:
180+
path = d.path or ""
181+
top = path.split("[")[0].split(".")[0]
182+
sections.add(top)
183+
assert "events" in sections, f"Event diffs not detected in {sections}"
184+
assert "state" in sections, f"State diffs not detected in {sections}"
185+
assert "memories" in sections, f"Memory diffs not detected in {sections}"
186+
assert "tracks" in sections, f"Track diffs not detected in {sections}"

0 commit comments

Comments
 (0)