zep-protocol 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: zep-protocol
3
+ Version: 0.1.0
4
+ Summary: ZEP — Zero Echo Pipe: lightweight inter-program communication protocol
5
+ Author: zeroechosoft
6
+ License: MIT
7
+ Project-URL: Homepage, https://saintiron82.github.io/ZeroEchoPipe/
8
+ Project-URL: Repository, https://github.com/saintiron82/ZeroEchoPipe
9
+ Project-URL: Documentation, https://saintiron82.github.io/ZeroEchoPipe/
10
+ Keywords: ipc,protocol,messaging,agent,rpc
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: System :: Networking
21
+ Requires-Python: >=3.10
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zep-protocol"
7
+ version = "0.1.0"
8
+ description = "ZEP — Zero Echo Pipe: lightweight inter-program communication protocol"
9
+ requires-python = ">=3.10"
10
+ license = {text = "MIT"}
11
+ keywords = ["ipc", "protocol", "messaging", "agent", "rpc"]
12
+ authors = [
13
+ {name = "zeroechosoft"},
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Libraries",
25
+ "Topic :: System :: Networking",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://saintiron82.github.io/ZeroEchoPipe/"
30
+ Repository = "https://github.com/saintiron82/ZeroEchoPipe"
31
+ Documentation = "https://saintiron82.github.io/ZeroEchoPipe/"
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["zep*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,128 @@
1
+ """Tests for BaseAgent framework — decorator binding, agent-to-agent communication."""
2
+
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ import time
7
+ import unittest
8
+
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
10
+
11
+ from zep.agent import BaseAgent, method, on_event
12
+ from zep.peer import RemoteError
13
+ from zep.transport.file import FileTransport
14
+
15
+
16
+ class EchoAgent(BaseAgent):
17
+ """Simple agent that echoes params and tracks events."""
18
+
19
+ def __init__(self, *args, **kwargs):
20
+ self.received_events = []
21
+ super().__init__(*args, **kwargs)
22
+
23
+ @method("echo")
24
+ def handle_echo(self, params):
25
+ return params
26
+
27
+ @method("add")
28
+ def handle_add(self, params):
29
+ return {"sum": params["a"] + params["b"]}
30
+
31
+ @on_event("notify")
32
+ def handle_notify(self, params):
33
+ self.received_events.append(params)
34
+
35
+
36
+ class TestAgentBasic(unittest.TestCase):
37
+ """Test BaseAgent decorator binding and method routing."""
38
+
39
+ def _setup_agents(self):
40
+ tmp_dir = tempfile.TemporaryDirectory()
41
+ transport_a = FileTransport(tmp_dir.name)
42
+ transport_b = FileTransport(tmp_dir.name)
43
+
44
+ server = EchoAgent("server", transport_a, session="test")
45
+ client = BaseAgent("client", transport_b, session="test")
46
+
47
+ server.run(blocking=False)
48
+
49
+ def cleanup():
50
+ server.stop()
51
+ transport_a.close()
52
+ transport_b.close()
53
+ tmp_dir.cleanup()
54
+
55
+ self.addCleanup(cleanup)
56
+ return server, client
57
+
58
+ def test_echo(self):
59
+ server, client = self._setup_agents()
60
+ result = client.call("server", "echo", {"msg": "hello"}, timeout_ms=5000)
61
+ self.assertEqual(result, {"msg": "hello"})
62
+
63
+ def test_add(self):
64
+ server, client = self._setup_agents()
65
+ result = client.call("server", "add", {"a": 3, "b": 4}, timeout_ms=5000)
66
+ self.assertEqual(result, {"sum": 7})
67
+
68
+ def test_event(self):
69
+ server, client = self._setup_agents()
70
+ client.emit("server", "notify", {"level": "info", "msg": "test"})
71
+ time.sleep(0.1)
72
+ self.assertEqual(len(server.received_events), 1)
73
+ self.assertEqual(server.received_events[0]["msg"], "test")
74
+
75
+ def test_capabilities(self):
76
+ server, client = self._setup_agents()
77
+ caps = client.call("server", "_capabilities", {}, timeout_ms=5000)
78
+ self.assertEqual(caps["name"], "server")
79
+ self.assertEqual(caps["agent_type"], "EchoAgent")
80
+ self.assertIn("echo", caps["methods"])
81
+ self.assertIn("add", caps["methods"])
82
+
83
+ def test_unknown_method(self):
84
+ server, client = self._setup_agents()
85
+ with self.assertRaises(RemoteError) as ctx:
86
+ client.call("server", "nonexistent", {}, timeout_ms=5000)
87
+ self.assertEqual(ctx.exception.code, "METHOD_NOT_FOUND")
88
+
89
+
90
+ class TestAgentToAgent(unittest.TestCase):
91
+ """Two agents communicating with each other."""
92
+
93
+ def test_bidirectional(self):
94
+ with tempfile.TemporaryDirectory() as tmp_dir:
95
+ transport_a = FileTransport(tmp_dir)
96
+ transport_b = FileTransport(tmp_dir)
97
+
98
+ class AgentA(BaseAgent):
99
+ @method("greet")
100
+ def handle_greet(self, params):
101
+ return {"reply": f"Hello, {params['name']}!"}
102
+
103
+ class AgentB(BaseAgent):
104
+ @method("status")
105
+ def handle_status(self, params):
106
+ return {"online": True}
107
+
108
+ a = AgentA("alice", transport_a, session="s1")
109
+ b = AgentB("bob", transport_b, session="s1")
110
+
111
+ a.run(blocking=False)
112
+ b.run(blocking=False)
113
+
114
+ try:
115
+ # B calls A
116
+ result = b.call("alice", "greet", {"name": "Bob"}, timeout_ms=5000)
117
+ self.assertEqual(result, {"reply": "Hello, Bob!"})
118
+
119
+ # A calls B
120
+ result = a.call("bob", "status", {}, timeout_ms=5000)
121
+ self.assertEqual(result, {"online": True})
122
+ finally:
123
+ a.stop()
124
+ b.stop()
125
+
126
+
127
+ if __name__ == "__main__":
128
+ unittest.main()
@@ -0,0 +1,98 @@
1
+ """ZEP protocol conformance test suite.
2
+
3
+ Loads test cases from the shared conformance manifest and validates
4
+ parse, parse-invalid, and serialize behaviour against expected outputs.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ import unittest
11
+
12
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
13
+
14
+ from zep.message import ValidationError, parse, serialize
15
+
16
+ CONFORMANCE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "conformance")
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Helpers
21
+ # ---------------------------------------------------------------------------
22
+
23
+ def _load_json(path: str):
24
+ with open(path, encoding="utf-8") as f:
25
+ return json.load(f)
26
+
27
+
28
+ def _load_raw(path: str) -> str:
29
+ with open(path, encoding="utf-8") as f:
30
+ return f.read()
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Manifest (loaded once at module level)
35
+ # ---------------------------------------------------------------------------
36
+
37
+ _MANIFEST = _load_json(os.path.join(CONFORMANCE_DIR, "manifest.json"))
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Test cases
42
+ # ---------------------------------------------------------------------------
43
+
44
+ class TestParseValid(unittest.TestCase):
45
+ """Input messages that must parse successfully."""
46
+
47
+ def test_parse_valid_cases(self):
48
+ for case in _MANIFEST["suites"]["parse_valid"]:
49
+ case_name = case.rsplit("/", 1)[-1]
50
+ with self.subTest(case=case_name):
51
+ raw = _load_raw(
52
+ os.path.join(CONFORMANCE_DIR, f"{case}.input.json")
53
+ ).rstrip("\n")
54
+ expected_parsed = _load_json(
55
+ os.path.join(CONFORMANCE_DIR, f"{case}.expected.json")
56
+ )["parsed"]
57
+ result = parse(raw)
58
+ self.assertEqual(result, expected_parsed)
59
+
60
+
61
+ class TestParseInvalid(unittest.TestCase):
62
+ """Input messages that must be rejected with the correct error."""
63
+
64
+ def test_parse_invalid_cases(self):
65
+ for case in _MANIFEST["suites"]["parse_invalid"]:
66
+ case_name = case.rsplit("/", 1)[-1]
67
+ with self.subTest(case=case_name):
68
+ raw = _load_raw(
69
+ os.path.join(CONFORMANCE_DIR, f"{case}.input.json")
70
+ ).rstrip("\n")
71
+ expected = _load_json(
72
+ os.path.join(CONFORMANCE_DIR, f"{case}.expected.json")
73
+ )
74
+ with self.assertRaises(ValidationError) as ctx:
75
+ parse(raw)
76
+ self.assertEqual(ctx.exception.code, expected["error_code"])
77
+ self.assertEqual(ctx.exception.field, expected["error_field"])
78
+
79
+
80
+ class TestSerialize(unittest.TestCase):
81
+ """Field dicts that must serialize to byte-exact JSONL strings."""
82
+
83
+ def test_serialize_cases(self):
84
+ for case in _MANIFEST["suites"]["serialize"]:
85
+ case_name = case.rsplit("/", 1)[-1]
86
+ with self.subTest(case=case_name):
87
+ fields = _load_json(
88
+ os.path.join(CONFORMANCE_DIR, f"{case}.fields.json")
89
+ )
90
+ expected_raw = _load_raw(
91
+ os.path.join(CONFORMANCE_DIR, f"{case}.expected.jsonl")
92
+ )
93
+ result = serialize(fields, profile="jsonl")
94
+ self.assertEqual(result, expected_raw)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ unittest.main()
@@ -0,0 +1,155 @@
1
+ """Tests for Peer advanced features — reserved methods, routing, shutdown."""
2
+
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ import threading
7
+ import time
8
+ import unittest
9
+
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
11
+
12
+ from zep.peer import Peer, CallTimeout, RemoteError
13
+ from zep.transport.file import FileTransport
14
+
15
+
16
+ class TestReservedMethods(unittest.TestCase):
17
+ """Test _capabilities, _ping, _shutdown protocol methods."""
18
+
19
+ def _make_peers(self):
20
+ tmp_dir = tempfile.TemporaryDirectory()
21
+ transport_a = FileTransport(tmp_dir.name)
22
+ transport_b = FileTransport(tmp_dir.name)
23
+
24
+ engine = Peer(transport_a, "engine", session="test_session",
25
+ capabilities={"version": "0.1.0"})
26
+ agent = Peer(transport_b, "agent", session="test_session")
27
+
28
+ engine.bind("echo", lambda p: p)
29
+
30
+ stop = threading.Event()
31
+
32
+ def engine_loop():
33
+ while not stop.is_set():
34
+ engine.poll_once()
35
+ stop.wait(0.001)
36
+
37
+ t = threading.Thread(target=engine_loop, daemon=True)
38
+ t.start()
39
+
40
+ def cleanup():
41
+ stop.set()
42
+ t.join(timeout=1)
43
+ transport_a.close()
44
+ transport_b.close()
45
+ tmp_dir.cleanup()
46
+
47
+ self.addCleanup(cleanup)
48
+ return engine, agent
49
+
50
+ def test_capabilities(self):
51
+ engine, agent = self._make_peers()
52
+ result = agent.call("engine", "_capabilities", {}, timeout_ms=5000)
53
+ self.assertEqual(result["name"], "engine")
54
+ self.assertEqual(result["schema"], "zep.v0.1")
55
+ self.assertIn("echo", result["methods"])
56
+ self.assertEqual(result["version"], "0.1.0")
57
+
58
+ def test_ping(self):
59
+ engine, agent = self._make_peers()
60
+ result = agent.call("engine", "_ping", {}, timeout_ms=5000)
61
+ self.assertTrue(result["pong"])
62
+ self.assertIn("timestamp", result)
63
+
64
+ def test_shutdown(self):
65
+ engine, agent = self._make_peers()
66
+ shutdown_called = []
67
+ engine.on_shutdown(lambda: shutdown_called.append(True))
68
+
69
+ result = agent.call("engine", "_shutdown", {}, timeout_ms=5000)
70
+ self.assertTrue(result["acknowledged"])
71
+ self.assertTrue(engine.is_shutdown)
72
+ self.assertEqual(len(shutdown_called), 1)
73
+
74
+ def test_reserved_method_binding_blocked(self):
75
+ engine, agent = self._make_peers()
76
+ with self.assertRaises(ValueError):
77
+ engine.bind("_secret", lambda p: p)
78
+
79
+
80
+ class TestRoutingSymmetry(unittest.TestCase):
81
+ """Test §3.3 routing symmetry verification."""
82
+
83
+ def test_session_mismatch_ignored(self):
84
+ """Response with wrong session should be ignored → timeout."""
85
+ with tempfile.TemporaryDirectory() as tmp_dir:
86
+ transport = FileTransport(tmp_dir)
87
+
88
+ from zep.message import serialize
89
+ # Agent sends call
90
+ agent = Peer(transport, "agent", session="s1")
91
+
92
+ # Manually craft a response with wrong session
93
+ bad_response = {
94
+ "id": "resp1",
95
+ "session": "wrong_session",
96
+ "from": "engine",
97
+ "to": "agent",
98
+ "type": "response",
99
+ "timestamp": "2026-03-13T12:00:00.000Z",
100
+ "meta": {"schema": "zep.v0.1"},
101
+ "reply_to": "nonexistent",
102
+ "result": {"ok": True},
103
+ }
104
+ transport.send("agent", "engine", serialize(bad_response))
105
+
106
+ # Agent polls — should ignore the mismatched response
107
+ agent.poll_once()
108
+ # No crash = success. The response is silently ignored.
109
+
110
+
111
+ class TestBindEvent(unittest.TestCase):
112
+ """Test separate event handler registration."""
113
+
114
+ def test_bind_event_separate(self):
115
+ with tempfile.TemporaryDirectory() as tmp_dir:
116
+ transport_a = FileTransport(tmp_dir)
117
+ transport_b = FileTransport(tmp_dir)
118
+
119
+ engine = Peer(transport_a, "engine", session="s1")
120
+ agent = Peer(transport_b, "agent", session="s1")
121
+
122
+ call_results = []
123
+ event_results = []
124
+
125
+ engine.bind("action", lambda p: call_results.append(p) or {"ok": True})
126
+ engine.bind_event("action", lambda p: event_results.append(p))
127
+
128
+ stop = threading.Event()
129
+
130
+ def engine_loop():
131
+ while not stop.is_set():
132
+ engine.poll_once()
133
+ stop.wait(0.001)
134
+
135
+ t = threading.Thread(target=engine_loop, daemon=True)
136
+ t.start()
137
+
138
+ try:
139
+ # Event uses event handler
140
+ agent.emit("engine", "action", {"type": "event"})
141
+ time.sleep(0.1)
142
+
143
+ # Call uses call handler
144
+ result = agent.call("engine", "action", {"type": "call"}, timeout_ms=5000)
145
+
146
+ self.assertEqual(len(event_results), 1)
147
+ self.assertEqual(event_results[0]["type"], "event")
148
+ self.assertEqual(result, {"ok": True})
149
+ finally:
150
+ stop.set()
151
+ t.join(timeout=1)
152
+
153
+
154
+ if __name__ == "__main__":
155
+ unittest.main()
@@ -0,0 +1,106 @@
1
+ """ZEP Roundtrip Test — two peers communicate via file transport."""
2
+
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ import threading
7
+ import time
8
+ import unittest
9
+
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
11
+
12
+ from zep.peer import Peer, CallTimeout, RemoteError
13
+ from zep.transport.file import FileTransport
14
+
15
+
16
+ class TestRoundtrip(unittest.TestCase):
17
+
18
+ def _make_peers(self):
19
+ """Create a pair of peers connected via file transport."""
20
+ tmp_dir = tempfile.TemporaryDirectory()
21
+ self._tmp_dir = tmp_dir
22
+
23
+ transport_a = FileTransport(tmp_dir.name)
24
+ transport_b = FileTransport(tmp_dir.name)
25
+
26
+ engine = Peer(transport_a, "engine", session="test_session")
27
+ agent = Peer(transport_b, "agent", session="test_session")
28
+
29
+ stop = threading.Event()
30
+
31
+ def engine_loop():
32
+ while not stop.is_set():
33
+ engine.poll_once()
34
+ stop.wait(0.001)
35
+
36
+ t = threading.Thread(target=engine_loop, daemon=True)
37
+ t.start()
38
+
39
+ def cleanup():
40
+ stop.set()
41
+ t.join(timeout=1)
42
+ transport_a.close()
43
+ transport_b.close()
44
+ tmp_dir.cleanup()
45
+
46
+ self.addCleanup(cleanup)
47
+
48
+ return engine, agent
49
+
50
+ def test_basic_call(self):
51
+ """Program peer registers method, agent peer calls it."""
52
+ engine, agent = self._make_peers()
53
+ engine.bind("get_status", lambda p: {"health": 100, "fps": 60})
54
+
55
+ result = agent.call("engine", "get_status", {}, timeout_ms=5000)
56
+ self.assertEqual(result, {"health": 100, "fps": 60})
57
+
58
+ def test_echo(self):
59
+ """Echo handler returns params as-is."""
60
+ engine, agent = self._make_peers()
61
+ engine.bind("echo", lambda p: p)
62
+
63
+ result = agent.call("engine", "echo", {"msg": "hello"}, timeout_ms=5000)
64
+ self.assertEqual(result, {"msg": "hello"})
65
+
66
+ def test_unknown_method(self):
67
+ """Calling an unregistered method raises METHOD_NOT_FOUND."""
68
+ engine, agent = self._make_peers()
69
+
70
+ with self.assertRaises(RemoteError) as ctx:
71
+ agent.call("engine", "nonexistent", {}, timeout_ms=5000)
72
+ self.assertEqual(ctx.exception.code, "METHOD_NOT_FOUND")
73
+
74
+ def test_event(self):
75
+ """Fire-and-forget event is received by the engine."""
76
+ engine, agent = self._make_peers()
77
+ received = []
78
+ engine.bind("on_ping", lambda p: received.append(p))
79
+
80
+ agent.emit("engine", "on_ping", {"ts": "2026-03-13T12:00:00.000Z"})
81
+
82
+ time.sleep(0.1)
83
+ engine.poll_once()
84
+
85
+ self.assertEqual(len(received), 1)
86
+
87
+ def test_timeout(self):
88
+ """Extremely short timeout raises CallTimeout."""
89
+ engine, agent = self._make_peers()
90
+
91
+ with self.assertRaises(CallTimeout):
92
+ agent.call("engine", "get_status", {}, timeout_ms=1)
93
+
94
+ def test_internal_error(self):
95
+ """Handler that raises an exception produces INTERNAL_ERROR."""
96
+ engine, agent = self._make_peers()
97
+ engine.bind("crash", lambda p: 1 / 0)
98
+
99
+ with self.assertRaises(RemoteError) as ctx:
100
+ agent.call("engine", "crash", {}, timeout_ms=5000)
101
+ self.assertEqual(ctx.exception.code, "INTERNAL_ERROR")
102
+ self.assertTrue(ctx.exception.retryable)
103
+
104
+
105
+ if __name__ == "__main__":
106
+ unittest.main()
@@ -0,0 +1,122 @@
1
+ """Scenario-based conformance test runner for ZEP.
2
+
3
+ Interprets scenario JSON files that define step-by-step message exchanges
4
+ between peers, verifying state transitions per spec section 3.
5
+ """
6
+
7
+ import glob
8
+ import json
9
+ import os
10
+ import sys
11
+ import tempfile
12
+ import unittest
13
+
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
15
+
16
+ from zep.message import serialize, parse
17
+ from zep.transport.file import FileTransport
18
+
19
+ SCENARIO_DIR = os.path.join(
20
+ os.path.dirname(__file__), "..", "..", "conformance", "scenarios"
21
+ )
22
+
23
+
24
+ class TestScenario(unittest.TestCase):
25
+
26
+ def test_scenarios(self):
27
+ pattern = os.path.join(SCENARIO_DIR, "*.scenario.json")
28
+ scenario_files = sorted(glob.glob(pattern))
29
+ self.assertTrue(scenario_files, f"No scenario files found in {SCENARIO_DIR}")
30
+
31
+ for path in scenario_files:
32
+ name = os.path.basename(path)
33
+ with self.subTest(scenario=name):
34
+ with open(path, "r") as f:
35
+ scenario = json.load(f)
36
+ self._run_scenario(scenario)
37
+
38
+ def _run_scenario(self, scenario):
39
+ pending_calls = {} # msg_id -> "pending" | "completed" | "failed" | "timeout"
40
+ call_sessions = {} # msg_id -> session (for symmetry check)
41
+
42
+ with tempfile.TemporaryDirectory() as base_dir:
43
+ transport = FileTransport(base_dir)
44
+
45
+ for step in scenario["steps"]:
46
+ if "action" in step:
47
+ self._exec_action(step, transport, pending_calls, call_sessions)
48
+ elif "assert_state" in step:
49
+ self._exec_assert(step, pending_calls)
50
+
51
+ transport.close()
52
+
53
+ def _exec_action(self, step, transport, pending_calls, call_sessions):
54
+ action = step["action"]
55
+
56
+ if action == "send":
57
+ message = step["message"]
58
+ serialized = serialize(message, profile="jsonl")
59
+ transport.send(
60
+ to_peer=message["to"],
61
+ from_peer=message["from"],
62
+ data=serialized,
63
+ )
64
+ if message["type"] == "call":
65
+ pending_calls[message["id"]] = "pending"
66
+ call_sessions[message["id"]] = message["session"]
67
+
68
+ elif action == "receive":
69
+ raw_messages = transport.recv(step["actor"])
70
+ expected_id = step["expect_message_id"]
71
+ parsed_msg = None
72
+
73
+ for raw in raw_messages:
74
+ candidate = parse(raw)
75
+ if candidate["id"] == expected_id:
76
+ parsed_msg = candidate
77
+ break
78
+
79
+ self.assertIsNotNone(
80
+ parsed_msg,
81
+ f"Expected message {expected_id} not found "
82
+ f"for actor {step['actor']}",
83
+ )
84
+
85
+ if (
86
+ parsed_msg["type"] in ("response", "error")
87
+ and "reply_to" in parsed_msg
88
+ ):
89
+ reply_to = parsed_msg["reply_to"]
90
+ if reply_to in pending_calls:
91
+ original_session = call_sessions.get(reply_to)
92
+ if original_session and parsed_msg["session"] != original_session:
93
+ pass # session mismatch: ignore per spec 3.4
94
+ elif parsed_msg["type"] == "response":
95
+ pending_calls[reply_to] = "completed"
96
+ elif parsed_msg["type"] == "error":
97
+ pending_calls[reply_to] = "failed"
98
+
99
+ def _exec_assert(self, step, pending_calls):
100
+ expected = step["assert_state"].get("pending_calls", {})
101
+ for msg_id, expected_status in expected.items():
102
+ if expected_status == "timeout":
103
+ actual = pending_calls.get(msg_id)
104
+ if actual == "pending":
105
+ pending_calls[msg_id] = "timeout"
106
+ self.assertEqual(
107
+ pending_calls.get(msg_id),
108
+ "timeout",
109
+ f"pending_calls[{msg_id}]: expected timeout, "
110
+ f"got {pending_calls.get(msg_id)}",
111
+ )
112
+ else:
113
+ self.assertEqual(
114
+ pending_calls.get(msg_id),
115
+ expected_status,
116
+ f"pending_calls[{msg_id}]: expected {expected_status}, "
117
+ f"got {pending_calls.get(msg_id)}",
118
+ )
119
+
120
+
121
+ if __name__ == "__main__":
122
+ unittest.main()