Coverage for agentos/tests/test_lifecycle.py: 0%
265 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 07:12 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 07:12 +0800
1"""Tests for agentos.core.lifecycle — LifecycleManager, hooks, probes, reports."""
3import asyncio
5import pytest
7from agentos.core.lifecycle import (
8 ComponentHealth,
9 ComponentStatus,
10 LifecycleHook,
11 LifecycleManager,
12 LifecyclePhase,
13 LifecycleReport,
14 get_lifecycle,
15)
17# ============================================================================
18# LifecycleHook
19# ============================================================================
22class TestLifecycleHook:
23 def test_defaults(self):
24 hook = LifecycleHook(name="test", phase=LifecyclePhase.SERVICES, fn=lambda: None)
25 assert hook.name == "test"
26 assert hook.phase == LifecyclePhase.SERVICES
27 assert hook.timeout_seconds == 30.0
28 assert hook.critical is True
29 assert hook.weight == 50
30 assert hook.retries == 0
32 def test_custom(self):
33 hook = LifecycleHook(
34 name="test", phase=LifecyclePhase.API, fn=lambda: None,
35 critical=False, weight=10, retries=3,
36 )
37 assert hook.critical is False
38 assert hook.weight == 10
39 assert hook.retries == 3
42# ============================================================================
43# ComponentHealth
44# ============================================================================
47class TestComponentHealth:
48 def test_defaults(self):
49 ch = ComponentHealth(name="db")
50 assert ch.name == "db"
51 assert ch.status == ComponentStatus.UNINITIALIZED
52 assert ch.phase is None
53 assert ch.message == ""
55 def test_custom(self):
56 ch = ComponentHealth(
57 name="db", status=ComponentStatus.HEALTHY,
58 phase=LifecyclePhase.INFRA, message="ok",
59 )
60 assert ch.status == ComponentStatus.HEALTHY
63# ============================================================================
64# LifecycleReport
65# ============================================================================
68class TestLifecycleReport:
69 def test_is_healthy(self):
70 r = LifecycleReport(overall_status=ComponentStatus.HEALTHY)
71 assert r.is_healthy is True
73 def test_not_healthy(self):
74 r = LifecycleReport(overall_status=ComponentStatus.UNHEALTHY)
75 assert r.is_healthy is False
77 def test_is_ready_healthy(self):
78 r = LifecycleReport(overall_status=ComponentStatus.HEALTHY)
79 assert r.is_ready is True
81 def test_is_ready_degraded(self):
82 r = LifecycleReport(overall_status=ComponentStatus.DEGRADED)
83 assert r.is_ready is True
85 def test_not_ready(self):
86 r = LifecycleReport(overall_status=ComponentStatus.UNINITIALIZED)
87 assert r.is_ready is False
90# ============================================================================
91# LifecycleManager
92# ============================================================================
95class TestLifecycleManagerCore:
96 @pytest.mark.asyncio
97 async def test_single_startup_hook(self):
98 lm = LifecycleManager()
99 ran = []
101 @lm.on_startup(name="s1")
102 async def startup():
103 ran.append(1)
105 report = await lm.start()
106 assert ran == [1]
107 assert report.overall_status == ComponentStatus.HEALTHY
109 @pytest.mark.asyncio
110 async def test_sync_startup_hook(self):
111 lm = LifecycleManager()
112 ran = []
114 @lm.on_startup(name="s1")
115 def startup():
116 ran.append(1)
118 report = await lm.start()
119 assert ran == [1]
120 assert report.overall_status == ComponentStatus.HEALTHY
122 @pytest.mark.asyncio
123 async def test_phase_ordering(self):
124 lm = LifecycleManager()
125 order = []
127 @lm.on_startup(phase=LifecyclePhase.API)
128 async def api():
129 order.append("api")
131 @lm.on_startup(phase=LifecyclePhase.CONFIG)
132 async def cfg():
133 order.append("config")
135 @lm.on_startup(phase=LifecyclePhase.SECURITY)
136 async def sec():
137 order.append("security")
139 await lm.start()
140 assert order == ["config", "security", "api"]
142 @pytest.mark.asyncio
143 async def test_weight_ordering_same_phase(self):
144 lm = LifecycleManager()
145 order = []
147 @lm.on_startup(phase=LifecyclePhase.SERVICES, weight=90)
148 async def s3():
149 order.append("s3")
151 @lm.on_startup(phase=LifecyclePhase.SERVICES, weight=10)
152 async def s1():
153 order.append("s1")
155 @lm.on_startup(phase=LifecyclePhase.SERVICES, weight=50)
156 async def s2():
157 order.append("s2")
159 await lm.start()
160 assert order == ["s1", "s2", "s3"]
162 @pytest.mark.asyncio
163 async def test_critical_hook_failure(self):
164 lm = LifecycleManager()
166 @lm.on_startup(name="bad", critical=True)
167 async def bad():
168 raise RuntimeError("boom")
170 @lm.on_startup(name="after", phase=LifecyclePhase.API)
171 async def after():
172 pass
174 report = await lm.start()
175 assert report.overall_status == ComponentStatus.UNHEALTHY
176 assert report.components["bad"].status == ComponentStatus.UNHEALTHY
178 @pytest.mark.asyncio
179 async def test_noncritical_hook_failure(self):
180 lm = LifecycleManager()
181 ran = []
183 @lm.on_startup(name="bad", critical=False)
184 async def bad():
185 raise RuntimeError("boom")
187 @lm.on_startup(name="after", phase=LifecyclePhase.API)
188 async def after():
189 ran.append("after")
191 report = await lm.start()
192 assert ran == ["after"]
193 assert report.overall_status == ComponentStatus.HEALTHY
195 @pytest.mark.asyncio
196 async def test_hook_timeout(self):
197 lm = LifecycleManager()
199 @lm.on_startup(name="slow", timeout_seconds=0.01)
200 async def slow():
201 await asyncio.sleep(99)
203 report = await lm.start()
204 assert report.overall_status == ComponentStatus.UNHEALTHY
206 @pytest.mark.asyncio
207 async def test_hook_retries_succeed(self):
208 lm = LifecycleManager()
209 attempts = []
211 @lm.on_startup(name="retry", retries=2, retry_delay=0.01)
212 async def retry():
213 attempts.append(1)
214 if len(attempts) < 3:
215 raise RuntimeError("fail")
217 report = await lm.start()
218 assert len(attempts) == 3
219 assert report.overall_status == ComponentStatus.HEALTHY
221 @pytest.mark.asyncio
222 async def test_hook_retries_exhausted(self):
223 lm = LifecycleManager()
225 @lm.on_startup(name="retry", retries=1, retry_delay=0.01, critical=True)
226 async def retry():
227 raise RuntimeError("always fail")
229 report = await lm.start()
230 assert report.overall_status == ComponentStatus.UNHEALTHY
233class TestLifecycleManagerShutdown:
234 @pytest.mark.asyncio
235 async def test_shutdown_reverse_order(self):
236 lm = LifecycleManager()
237 order = []
239 @lm.on_shutdown(name="a")
240 async def a():
241 order.append("a")
243 @lm.on_shutdown(name="b")
244 async def b():
245 order.append("b")
247 @lm.on_shutdown(name="c")
248 async def c():
249 order.append("c")
251 report = await lm.shutdown()
252 assert order == ["c", "b", "a"]
253 assert report.overall_status == ComponentStatus.STOPPED
255 @pytest.mark.asyncio
256 async def test_shutdown_sync_hook(self):
257 lm = LifecycleManager()
258 ran = []
260 @lm.on_shutdown(name="sync")
261 def sync():
262 ran.append(1)
264 await lm.shutdown()
265 assert ran == [1]
267 @pytest.mark.asyncio
268 async def test_shutdown_idempotent(self):
269 lm = LifecycleManager()
270 count = 0
272 @lm.on_shutdown(name="x")
273 async def x():
274 nonlocal count
275 count += 1
277 await lm.shutdown()
278 await lm.shutdown()
279 assert count == 1
281 @pytest.mark.asyncio
282 async def test_shutdown_after_start_stops(self):
283 lm = LifecycleManager()
285 @lm.on_shutdown(name="close")
286 async def close():
287 pass
289 await lm.start()
290 report = await lm.shutdown()
291 assert report.overall_status == ComponentStatus.STOPPED
292 assert lm.is_live() is False
295class TestLifecycleManagerProbes:
296 def test_initial_probes(self):
297 lm = LifecycleManager()
298 assert lm.is_ready() is False
299 assert lm.is_live() is True
301 @pytest.mark.asyncio
302 async def test_ready_after_start(self):
303 lm = LifecycleManager()
304 await lm.start()
305 assert lm.is_ready() is True
306 assert lm.is_live() is True
308 @pytest.mark.asyncio
309 async def test_not_ready_before_start(self):
310 lm = LifecycleManager()
312 @lm.on_startup(name="s1")
313 async def s1():
314 pass
316 assert lm.is_ready() is False
318 @pytest.mark.asyncio
319 async def test_live_after_startup_failure(self):
320 lm = LifecycleManager()
322 @lm.on_startup(name="bad", critical=True)
323 async def bad():
324 raise RuntimeError("fail")
326 await lm.start()
327 assert lm.is_ready() is False
328 assert lm.is_live() is False
331class TestLifecycleManagerContext:
332 @pytest.mark.asyncio
333 async def test_context_manager(self):
334 ran_start = False
335 ran_stop = False
337 lm = LifecycleManager()
339 @lm.on_startup(name="s")
340 async def s():
341 nonlocal ran_start
342 ran_start = True
344 @lm.on_shutdown(name="close")
345 async def close():
346 nonlocal ran_stop
347 ran_stop = True
349 async with lm:
350 assert ran_start is True
351 assert ran_stop is False
353 assert ran_stop is True
356class TestLifecycleManagerReport:
357 @pytest.mark.asyncio
358 async def test_report_after_start(self):
359 lm = LifecycleManager()
361 @lm.on_startup(name="db")
362 async def db():
363 pass
365 report = await lm.start()
366 assert report.overall_status == ComponentStatus.HEALTHY
367 assert "db" in report.components
368 assert report.components["db"].status == ComponentStatus.HEALTHY
369 assert report.startup_duration_ms >= 0
371 @pytest.mark.asyncio
372 async def test_report_after_shutdown(self):
373 lm = LifecycleManager()
374 report = await lm.shutdown()
375 assert report.overall_status == ComponentStatus.STOPPED
376 assert report.components == {}
379# ============================================================================
380# get_lifecycle singleton
381# ============================================================================
384class TestGetLifecycle:
385 def test_singleton(self):
386 lm1 = get_lifecycle()
387 lm2 = get_lifecycle()
388 assert lm1 is lm2
390 def test_initial_status(self):
391 import agentos.core.lifecycle as lc
392 lc._default_lifecycle = None
393 lm = get_lifecycle()
394 assert lm.is_ready() is False
395 assert lm.is_live() is True