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