Coverage for agentos/tests/test_a2a.py: 0%
228 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 12:29 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 12:29 +0800
1"""测试 A2A 协议 — Task, Message, Handoff, Client, Server。"""
3import time
5import pytest
7from agentos.protocols.a2a import (
8 A2AArtifact,
9 A2AHandoff,
10 A2AMessage,
11 A2AServer,
12 A2ASession,
13 A2ATask,
14 DataPart,
15 FilePart,
16 MessageRole,
17 TaskState,
18 TextPart,
19 new_handoff,
20 new_task,
21 part_from_dict,
22)
25class TestA2AParts:
26 def test_text_part_roundtrip(self):
27 tp = TextPart(text="hello", meta={"lang": "en"})
28 d = tp.to_dict()
29 assert d["type"] == "text"
30 tp2 = TextPart.from_dict(d)
31 assert tp2.text == "hello"
32 assert tp2.meta == {"lang": "en"}
34 def test_file_part_roundtrip(self):
35 fp = FilePart(
36 url="https://ex.com/f.pdf",
37 filename="report.pdf",
38 mime_type="application/pdf",
39 size=1024,
40 )
41 d = fp.to_dict()
42 fp2 = FilePart.from_dict(d)
43 assert fp2.filename == "report.pdf"
44 assert fp2.mime_type == "application/pdf"
46 def test_data_part_roundtrip(self):
47 dp = DataPart(data={"count": 42}, schema_uri="https://schema.org/result")
48 d = dp.to_dict()
49 dp2 = DataPart.from_dict(d)
50 assert dp2.data["count"] == 42
52 def test_part_from_dict_dispatcher(self):
53 d = {"type": "text", "text": "hi"}
54 p = part_from_dict(d)
55 assert isinstance(p, TextPart)
56 assert p.text == "hi"
58 d = {"type": "file", "filename": "x.txt"}
59 p = part_from_dict(d)
60 assert isinstance(p, FilePart)
62 d = {"type": "data", "data": {"a": 1}}
63 p = part_from_dict(d)
64 assert isinstance(p, DataPart)
67class TestA2AArtifact:
68 def test_roundtrip(self):
69 art = A2AArtifact(name="result.json", mime_type="application/json", blob=b'{"a":1}', size=8)
70 d = art.to_dict()
71 art2 = A2AArtifact.from_dict(d)
72 assert art2.name == "result.json"
73 assert art2.blob == b'{"a":1}'
75 def test_url_artifact(self):
76 art = A2AArtifact(name="image.png", url="https://cdn.ex/img.png")
77 d = art.to_dict()
78 assert "url" in d
79 art2 = A2AArtifact.from_dict(d)
80 assert art2.url == "https://cdn.ex/img.png"
83class TestA2AMessage:
84 def test_user_text(self):
85 msg = A2AMessage.user_text("hello world")
86 assert msg.role == MessageRole.USER
87 assert len(msg.parts) == 1
88 assert msg.parts[0].text == "hello world"
90 def test_agent_text(self):
91 msg = A2AMessage.agent_text("done")
92 assert msg.role == MessageRole.AGENT
93 assert msg.get_text() == "done"
95 def test_multipart_roundtrip(self):
96 msg = A2AMessage(
97 role=MessageRole.USER,
98 parts=[
99 TextPart(text="analyze this"),
100 FilePart(filename="data.csv"),
101 DataPart(data={"options": {"method": "pca"}}),
102 ],
103 )
104 d = msg.to_dict()
105 msg2 = A2AMessage.from_dict(d)
106 assert msg2.role == MessageRole.USER
107 assert len(msg2.parts) == 3
108 assert isinstance(msg2.parts[0], TextPart)
109 assert isinstance(msg2.parts[1], FilePart)
110 assert isinstance(msg2.parts[2], DataPart)
111 assert msg2.get_text() == "analyze this"
114class TestA2ATask:
115 def test_lifecycle(self):
116 task = A2ATask(input=A2AMessage.user_text("do something"))
117 assert task.state == TaskState.SUBMITTED
119 task.start_working()
120 assert task.state == TaskState.WORKING
122 task.complete(A2AMessage.agent_text("done"))
123 assert task.state == TaskState.COMPLETED
124 assert task.output.get_text() == "done"
125 assert task.is_terminal()
127 def test_fail(self):
128 task = A2ATask(input=A2AMessage.user_text("bad"))
129 task.start_working()
130 task.fail("something went wrong")
131 assert task.state == TaskState.FAILED
132 assert task.error == "something went wrong"
133 assert task.is_terminal()
135 def test_cancel(self):
136 task = A2ATask()
137 assert not task.is_terminal()
138 task.cancel()
139 assert task.state == TaskState.CANCELLED
140 assert task.is_terminal()
142 def test_cannot_start_non_submitted(self):
143 task = A2ATask()
144 task.start_working()
145 with pytest.raises(ValueError):
146 task.start_working()
148 def test_cannot_complete_non_working(self):
149 task = A2ATask()
150 with pytest.raises(ValueError):
151 task.complete()
153 def test_cannot_cancel_completed(self):
154 task = A2ATask()
155 task.start_working()
156 task.complete()
157 with pytest.raises(ValueError):
158 task.cancel()
160 def test_artifact_attachment(self):
161 task = A2ATask()
162 task.add_artifact(A2AArtifact(name="out.csv"))
163 task.add_artifact(A2AArtifact(name="out.png"))
164 assert len(task.artifacts) == 2
166 def test_json_roundtrip(self):
167 task = A2ATask(input=A2AMessage.user_text("hello"))
168 task.start_working()
169 task.complete(A2AMessage.agent_text("result"))
170 task.add_artifact(A2AArtifact(name="out.json", blob=b"{}"))
172 json_str = task.to_json()
173 task2 = A2ATask.from_json(json_str)
174 assert task2.task_id == task.task_id
175 assert task2.state == TaskState.COMPLETED
176 assert task2.input.get_text() == "hello"
177 assert task2.artifacts[0].name == "out.json"
179 def test_state_history(self):
180 task = A2ATask()
181 task.start_working()
182 task.complete()
183 assert len(task._state_history) == 2
184 assert task._state_history[0][0] == TaskState.SUBMITTED
185 assert task._state_history[1][0] == TaskState.WORKING
188class TestA2AHandoff:
189 def test_roundtrip(self):
190 task = A2ATask(input=A2AMessage.user_text("do x"))
191 ho = A2AHandoff(
192 source_agent="coordinator",
193 target_agent="worker",
194 task=task,
195 reason="delegation",
196 )
197 d = ho.to_dict()
198 ho2 = A2AHandoff.from_dict(d)
199 assert ho2.source_agent == "coordinator"
200 assert ho2.target_agent == "worker"
201 assert ho2.task.task_id == task.task_id
202 assert ho2.reason == "delegation"
204 def test_json_roundtrip(self):
205 task = A2ATask(input=A2AMessage.user_text("test"))
206 ho = A2AHandoff(source_agent="a", target_agent="b", task=task)
207 ho2 = A2AHandoff.from_json(ho.to_json())
208 assert ho2.source_agent == "a"
209 assert ho2.handoff_id == ho.handoff_id
212class TestA2ASession:
213 def test_basic(self):
214 sess = A2ASession()
215 sess.add_message(A2AMessage.user_text("hi"))
216 sess.add_message(A2AMessage.agent_text("hello"))
217 sess.add_task(A2ATask())
218 assert len(sess.history) == 2
219 assert len(sess.tasks) == 1
221 def test_get_last_n(self):
222 sess = A2ASession()
223 for i in range(5):
224 sess.add_message(A2AMessage.user_text(f"msg{i}"))
225 last3 = sess.get_last_n_messages(3)
226 assert len(last3) == 3
227 assert last3[-1].get_text() == "msg4"
230class TestA2Server:
231 @pytest.mark.asyncio
232 async def test_process_task_success(self):
233 server = A2AServer()
235 async def handler(task: A2ATask):
236 return A2AMessage.agent_text(f"processed: {task.input.get_text()}")
238 server.register_handler("worker", handler)
239 task = new_task("hello test", target_agent="worker")
240 result = await server.process_task(task.to_dict())
241 assert result["state"] == "completed"
242 assert "processed: hello test" in result["output"]["parts"][0]["text"]
244 @pytest.mark.asyncio
245 async def test_process_task_no_handler(self):
246 server = A2AServer()
247 task = new_task("hello", target_agent="nonexistent")
248 result = await server.process_task(task.to_dict())
249 assert result["state"] == "failed"
250 assert "No handler" in result["error"]
252 @pytest.mark.asyncio
253 async def test_process_task_handler_error(self):
254 server = A2AServer()
256 async def bad_handler(task):
257 raise ValueError("simulated error")
259 server.register_handler("bad", bad_handler)
260 task = new_task("test", target_agent="bad")
261 result = await server.process_task(task.to_dict())
262 assert result["state"] == "failed"
263 assert "simulated error" in result["error"]
265 def test_get_task(self):
266 server = A2AServer()
267 task = A2ATask(task_id="task-001")
268 server._tasks["task-001"] = task
269 assert server.get_task("task-001").task_id == "task-001"
270 assert server.get_task("nonexistent") is None
272 def test_list_tasks_by_state(self):
273 server = A2AServer()
274 t1 = A2ATask(task_id="t1")
275 t2 = A2ATask(task_id="t2")
276 t2.start_working()
277 t2.complete()
278 t3 = A2ATask(task_id="t3")
279 t3.fail("err")
280 server._tasks = {"t1": t1, "t2": t2, "t3": t3}
281 assert len(server.list_tasks()) == 3
282 assert len(server.list_tasks(TaskState.COMPLETED)) == 1
283 assert len(server.list_tasks(TaskState.FAILED)) == 1
285 def test_cleanup(self):
286 server = A2AServer()
287 old = A2ATask(task_id="old")
288 old.start_working()
289 old.complete()
290 old._updated = time.time() - 4000 # fake old
291 fresh = A2ATask(task_id="fresh")
292 server._tasks = {"old": old, "fresh": fresh}
293 n = server.cleanup_old(max_age_seconds=3600)
294 assert n == 1
295 assert "old" not in server._tasks
296 assert "fresh" in server._tasks
299class TestConvenience:
300 def test_new_task(self):
301 t = new_task("my task", target_agent="worker", priority="high")
302 assert t.input.get_text() == "my task"
303 assert t.meta["target_agent"] == "worker"
304 assert t.meta["priority"] == "high"
306 def test_new_handoff(self):
307 t = new_task("delegate me")
308 ho = new_handoff(t, source="a", target="b", reason="overload")
309 assert ho.source_agent == "a"
310 assert ho.target_agent == "b"
311 assert ho.reason == "overload"