Coverage for agentos/tests/test_subagent_parent_child.py: 0%
251 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"""测试 SubAgent 父子通信 — 状态共享、心跳、生命周期管理。"""
3import asyncio
4import pytest
6pytestmark = pytest.mark.asyncio
7from agentos.subagent import (
8 SubAgentManager,
9 SubAgentSpec,
10 ChildStatus,
11 ChildHeartbeat,
12 SharedState,
13 ChildContext,
14 ChildHandle,
15)
18class TestSharedState:
19 async def test_set_get(self):
20 ss = SharedState()
21 await ss.set("key1", "val1")
22 assert await ss.get("key1") == "val1"
23 assert await ss.get("missing", "def") == "def"
25 async def test_update_snapshot(self):
26 ss = SharedState()
27 await ss.update({"a": 1, "b": 2})
28 snap = await ss.snapshot()
29 assert snap == {"a": 1, "b": 2}
31 async def test_sync_ops(self):
32 ss = SharedState()
33 ss.set_sync("x", 42)
34 assert ss.get_sync("x") == 42
36 async def test_concurrent_writes(self):
37 ss = SharedState()
39 async def writer(key: str, n: int):
40 for i in range(n):
41 await ss.set(key, i)
42 await asyncio.sleep(0)
44 await asyncio.gather(writer("a", 20), writer("b", 20))
45 assert await ss.get("a") == 19
46 assert await ss.get("b") == 19
49class TestChildContext:
50 async def test_progress_report(self):
51 hbs = []
53 async def hb_cb(hb: ChildHeartbeat):
54 hbs.append(hb)
56 ctx = ChildContext("test-1", heartbeat_callback=hb_cb)
57 await ctx.report_progress(0.5, "step1", "half done")
58 assert ctx.progress == 0.5
59 assert len(hbs) == 1
60 assert hbs[0].progress == 0.5
61 assert hbs[0].current_step == "step1"
63 async def test_step_and_heartbeat(self):
64 hbs = []
66 async def hb_cb(hb: ChildHeartbeat):
67 hbs.append(hb)
69 ctx = ChildContext("test-2", heartbeat_callback=hb_cb)
70 await ctx.step(1, "init")
71 await ctx.send_heartbeat("alive")
72 assert len(hbs) == 1
73 assert hbs[0].iteration == 1
75 async def test_done(self):
76 hbs = []
78 async def hb_cb(hb: ChildHeartbeat):
79 hbs.append(hb)
81 ctx = ChildContext("test-3", heartbeat_callback=hb_cb)
82 await ctx.done("all good")
83 assert len(hbs) == 1
84 assert hbs[0].status == ChildStatus.COMPLETED
85 assert hbs[0].progress == 1.0
86 assert hbs[0].message == "all good"
88 async def test_fail(self):
89 hbs = []
91 async def hb_cb(hb: ChildHeartbeat):
92 hbs.append(hb)
94 ctx = ChildContext("test-4", heartbeat_callback=hb_cb)
95 await ctx.fail("something broke")
96 assert len(hbs) == 1
97 assert hbs[0].status == ChildStatus.FAILED
98 assert hbs[0].message == "something broke"
100 async def test_cancel_detection(self):
101 cancelled = [False]
103 def on_cancel():
104 return cancelled[0]
106 ctx = ChildContext("test-5", on_cancel=on_cancel)
107 assert not ctx.cancelled
108 status = await ctx.check_control()
109 assert status == ChildStatus.RUNNING
111 cancelled[0] = True
112 status = await ctx.check_control()
113 assert status == ChildStatus.CANCELLED
114 assert ctx.cancelled
116 async def test_pause_resume(self):
117 paused = [True]
118 resume_triggered = [False]
120 async def on_pause():
121 resume_triggered[0] = True
122 paused[0] = False
124 ctx = ChildContext("test-6", on_pause=on_pause)
125 ctx._paused = paused[0]
126 status = await ctx.check_control()
127 assert status == ChildStatus.PAUSED
128 assert resume_triggered[0]
131class TestChildHandle:
132 async def test_create_context(self):
133 handle = ChildHandle("h1", "do stuff", "fork")
134 ctx = handle.create_context()
135 assert ctx.agent_id == "h1"
136 assert handle.context is ctx
137 assert handle.shared_state is ctx.shared_state
139 async def test_pause_resume(self):
140 handle = ChildHandle("h2", "task", "fork")
141 handle.create_context()
142 assert handle.status == ChildStatus.IDLE
144 await handle.pause()
145 assert handle.status == ChildStatus.PAUSED
147 await handle.resume()
148 assert handle.status == ChildStatus.RUNNING
150 async def test_cancel(self):
151 handle = ChildHandle("h3", "task", "fork")
152 handle.create_context()
153 await handle.cancel()
154 assert handle.status == ChildStatus.CANCELLED
156 async def test_get_status(self):
157 handle = ChildHandle("h4", "analyze", "fork")
158 handle.create_context()
159 handle.info.progress = 0.7
160 handle.info.current_step = "parsing"
161 handle.info.iterations = 12
163 status = handle.get_status()
164 assert status["agent_id"] == "h4"
165 assert status["progress"] == 0.7
166 assert status["current_step"] == "parsing"
167 assert status["iterations"] == 12
168 assert "elapsed" in status
170 async def test_timeout_detection(self):
171 handle = ChildHandle("h5", "task", "fork", timeout=0.1)
172 await asyncio.sleep(0.15)
173 assert handle.check_timeout()
175 async def test_no_timeout_when_unset(self):
176 handle = ChildHandle("h6", "task", "fork", timeout=None)
177 assert not handle.check_timeout()
179 async def test_heartbeat_timeout(self):
180 handle = ChildHandle("h7", "task", "fork", heartbeat_interval=0.1)
181 await asyncio.sleep(0.35)
182 assert handle.check_heartbeat_timeout()
184 async def test_heartbeat_updates_info(self):
185 handle = ChildHandle("h8", "task", "fork")
186 handle.create_context()
187 await handle._receive_heartbeat(ChildHeartbeat(
188 agent_id="h8", progress=0.5, current_step="s1",
189 message="working", iteration=5,
190 ))
191 assert handle.info.progress == 0.5
192 assert handle.info.current_step == "s1"
193 assert handle.info.iterations == 5
195 async def test_shared_state_parent_child(self):
196 handle = ChildHandle("h9", "task", "fork")
197 ctx = handle.create_context()
199 await ctx.shared_state.set("data", [1, 2, 3])
200 val = await handle.shared_state.get("data")
201 assert val == [1, 2, 3]
203 await handle.shared_state.set("status", "ok")
204 assert await ctx.shared_state.get("status") == "ok"
207class TestSubAgentManager:
208 async def test_spawn_fork_with_child_context(self):
209 hbs = []
211 async def run_func(spec: SubAgentSpec, ctx: ChildContext):
212 await ctx.report_progress(0.3, "init")
213 await ctx.step(1, "load")
214 await ctx.report_progress(0.7, "process")
215 await ctx.done("success")
216 return ("success", 2)
218 mgr = SubAgentManager()
219 result = await mgr.spawn_fork("test task", run_func=run_func)
220 assert result.output == "success"
221 assert result.iterations == 2
222 assert result.handle is not None
223 assert result.handle.status == ChildStatus.COMPLETED
225 async def test_spawn_fork_failure(self):
226 async def run_func(spec, ctx):
227 await ctx.report_progress(0.1, "start")
228 raise ValueError("boom")
230 mgr = SubAgentManager()
231 result = await mgr.spawn_fork("bad task", run_func=run_func)
232 assert result.error == "boom"
233 assert result.handle.status == ChildStatus.FAILED
234 assert result.handle.info.error == "boom"
236 async def test_spawn_fork_pause_resume_flow(self):
237 """模拟父子协作:父暂停→子暂停→父恢复→子继续→完成。"""
238 state = {"phase": "init"}
240 async def run_func(spec, ctx: ChildContext):
241 state["phase"] = "running"
242 await ctx.report_progress(0.2, "step1")
244 # 检查控制信号
245 status = await ctx.check_control()
246 if status == ChildStatus.PAUSED:
247 state["phase"] = "paused"
249 # 再次检查(模拟恢复后继续)
250 status = await ctx.check_control()
251 if status == ChildStatus.RUNNING:
252 state["phase"] = "resumed"
254 await ctx.done("ok")
255 return ("ok", 3)
257 mgr = SubAgentManager()
259 # 启动
260 task = asyncio.create_task(
261 mgr.spawn_fork("pause test", run_func=run_func)
262 )
264 await asyncio.sleep(0.05) # 让子Agent跑到 step
265 handle = mgr.get_handle(task.result().handle.agent_id) if hasattr(task, 'result') else None
267 # 等task完成
268 result = await task
269 assert result.error is None or result.error == ""
270 assert state["phase"] in ("running", "paused", "resumed")
272 async def test_swarm_parallel(self):
273 results_log = []
275 async def run_func(spec: SubAgentSpec, ctx: ChildContext):
276 await ctx.report_progress(0.5, spec.task)
277 await asyncio.sleep(0.01)
278 await ctx.done(f"done_{spec.task}")
279 results_log.append(spec.task)
280 return (f"done_{spec.task}", 1)
282 mgr = SubAgentManager()
283 results = await mgr.spawn_swarm(
284 ["A", "B", "C"], run_func=run_func
285 )
286 assert len(results) == 3
287 assert len(results_log) == 3
288 for r in results:
289 assert r.handle is not None
290 assert r.handle.status == ChildStatus.COMPLETED
292 async def test_cancel_all(self):
293 async def run_func(spec, ctx: ChildContext):
294 await ctx.report_progress(0.1, "init")
295 for i in range(50):
296 await ctx.step(i, f"step_{i}")
297 status = await ctx.check_control()
298 if status == ChildStatus.CANCELLED:
299 return ("cancelled", i)
300 await asyncio.sleep(0.01)
301 return ("done", 50)
303 mgr = SubAgentManager()
304 t1 = asyncio.create_task(
305 mgr.spawn_fork("long task 1", run_func=run_func)
306 )
307 t2 = asyncio.create_task(
308 mgr.spawn_fork("long task 2", run_func=run_func)
309 )
311 await asyncio.sleep(0.05)
312 await mgr.cancel_all()
314 r1, r2 = await asyncio.gather(t1, t2)
315 assert r1.handle.status == ChildStatus.CANCELLED
316 assert r2.handle.status == ChildStatus.CANCELLED
318 async def test_list_children(self):
319 mgr = SubAgentManager()
320 r = await mgr.spawn_fork("task1")
321 children = mgr.list_children()
322 assert len(children) == 1
323 assert children[0]["agent_id"] == r.agent_id
325 async def test_cleanup(self):
326 mgr = SubAgentManager()
327 r = await mgr.spawn_fork("cleanup test")
328 assert len(mgr._agents) == 1
330 cleaned = await mgr.cleanup(max_age_seconds=-1.0)
331 assert cleaned == 1
332 assert len(mgr._agents) == 0
334 async def test_heartbeat_monitoring(self):
335 mgr = SubAgentManager()
336 handle = ChildHandle("hb-test", "task", "fork", timeout=0.05)
337 mgr._agents["hb-test"] = handle
338 handle.info.status = ChildStatus.RUNNING
340 monitor = asyncio.create_task(mgr.monitor_heartbeats(interval=0.02))
341 await asyncio.sleep(0.1)
342 monitor.cancel()
343 try:
344 await monitor
345 except asyncio.CancelledError:
346 pass
348 assert handle.status in (ChildStatus.TIMEOUT, ChildStatus.RUNNING)