Coverage for agentos/tests/test_1_1_4_features.py: 79%
186 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 17:01 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 17:01 +0800
1"""v1.1.4 新特性集成测试。"""
3from __future__ import annotations
5import pytest
7# ══════════════════════════════════════════════════════════════════════════════
8# ToolRiskRating 测试
9# ══════════════════════════════════════════════════════════════════════════════
12class TestToolRiskRating:
13 def test_risk_level_enum(self):
14 from agentos.tools.risk import ToolRiskLevel
16 assert ToolRiskLevel.LOW.value == "low"
17 assert ToolRiskLevel.CRITICAL.value == "critical"
18 assert len(list(ToolRiskLevel)) == 4
20 def test_risk_rating_defaults(self):
21 from agentos.tools.risk import ToolRiskRating
23 r = ToolRiskRating()
24 assert r.level.value == "medium"
25 assert r.reversible is True
26 assert r.requires_approval is False
27 assert r.requires_user_confirm() is False
29 def test_requires_confirm_high(self):
30 from agentos.tools.risk import ToolRiskLevel, ToolRiskRating
32 r = ToolRiskRating(level=ToolRiskLevel.HIGH)
33 assert r.requires_user_confirm() is True
35 def test_requires_confirm_critical(self):
36 from agentos.tools.risk import ToolRiskLevel, ToolRiskRating
38 r = ToolRiskRating(level=ToolRiskLevel.CRITICAL)
39 assert r.requires_user_confirm() is True
41 def test_requires_confirm_financial(self):
42 from agentos.tools.risk import ToolRiskRating
44 r = ToolRiskRating(financial_impact=True)
45 assert r.requires_user_confirm() is True
47 def test_get_risk_preset_list_files(self):
48 from agentos.tools.risk import ToolRiskLevel, get_risk_preset
50 r = get_risk_preset("list_files")
51 assert r is not None
52 assert r.level == ToolRiskLevel.LOW
54 def test_get_risk_preset_delete_file(self):
55 from agentos.tools.risk import ToolRiskLevel, get_risk_preset
57 r = get_risk_preset("delete_file")
58 assert r is not None
59 assert r.level == ToolRiskLevel.HIGH
60 assert r.requires_approval is True
62 def test_get_risk_preset_payment(self):
63 from agentos.tools.risk import ToolRiskLevel, get_risk_preset
65 r = get_risk_preset("execute_payment")
66 assert r is not None
67 assert r.level == ToolRiskLevel.CRITICAL
68 assert r.financial_impact is True
70 def test_get_risk_preset_case_insensitive(self):
71 from agentos.tools.risk import get_risk_preset
73 assert get_risk_preset("DELETE_FILE") is not None
75 def test_infer_risk_level_keyword_delete(self):
76 from agentos.tools.risk import ToolRiskLevel, infer_risk_level
78 r = infer_risk_level("purge_records", "delete all records")
79 assert r.level == ToolRiskLevel.HIGH
81 def test_infer_risk_level_keyword_write(self):
82 from agentos.tools.risk import ToolRiskLevel, infer_risk_level
84 r = infer_risk_level("update_profile")
85 assert r.level == ToolRiskLevel.MEDIUM
87 def test_infer_risk_level_default(self):
88 from agentos.tools.risk import ToolRiskLevel, infer_risk_level
90 r = infer_risk_level("get_status")
91 assert r.level == ToolRiskLevel.LOW
94# ══════════════════════════════════════════════════════════════════════════════
95# Middleware Pipeline 测试
96# ══════════════════════════════════════════════════════════════════════════════
99class TestMiddlewarePipeline:
100 @pytest.mark.asyncio
101 async def test_empty_pipeline_allows(self):
102 from agentos.core.middleware import MiddlewareContext, MiddlewarePhase, MiddlewarePipeline
104 pipe = MiddlewarePipeline()
105 ctx = MiddlewareContext(phase=MiddlewarePhase.PRE_LLM, prompt="hello")
106 decision = await pipe.pre_llm(ctx)
107 assert decision.allow is True
109 @pytest.mark.asyncio
110 async def test_blocking_middleware(self):
111 from agentos.core.middleware import (
112 AgentMiddleware,
113 MiddlewareContext,
114 MiddlewareDecision,
115 MiddlewarePhase,
116 MiddlewarePipeline,
117 )
119 class Blocker(AgentMiddleware):
120 name = "blocker"
122 @property
123 def phases(self):
124 return [MiddlewarePhase.PRE_LLM]
126 async def process(self, ctx):
127 return MiddlewareDecision(allow=False, reason="blocked by test", action="block")
129 pipe = MiddlewarePipeline([Blocker()])
130 ctx = MiddlewareContext(phase=MiddlewarePhase.PRE_LLM, prompt="test")
131 decision = await pipe.pre_llm(ctx)
132 assert decision.allow is False
133 assert "blocked by test" in decision.reason
135 @pytest.mark.asyncio
136 async def test_transform_middleware(self):
137 from agentos.core.middleware import (
138 AgentMiddleware,
139 MiddlewareContext,
140 MiddlewareDecision,
141 MiddlewarePhase,
142 MiddlewarePipeline,
143 )
145 class UpperCaseTransform(AgentMiddleware):
146 name = "upper"
148 @property
149 def phases(self):
150 return [MiddlewarePhase.PRE_LLM]
152 async def process(self, ctx):
153 if ctx.prompt:
154 new_ctx = MiddlewareContext(**{**ctx.__dict__})
155 new_ctx.prompt = ctx.prompt.upper()
156 return MiddlewareDecision(
157 allow=True, action="transform", modified_context=new_ctx
158 )
159 return MiddlewareDecision(allow=True)
161 pipe = MiddlewarePipeline([UpperCaseTransform()])
162 ctx = MiddlewareContext(phase=MiddlewarePhase.PRE_LLM, prompt="hello")
163 decision = await pipe.pre_llm(ctx)
164 assert decision.allow is True
165 assert decision.modified_context is not None
166 assert decision.modified_context.prompt == "HELLO"
168 @pytest.mark.asyncio
169 async def test_chain_add(self):
170 from agentos.core.middleware import MiddlewarePipeline
172 pipe = MiddlewarePipeline()
173 from agentos.core.middleware import AuditLogMiddleware
175 pipe.add(AuditLogMiddleware())
176 assert "audit_log" in pipe.middleware_names
178 @pytest.mark.asyncio
179 async def test_remove(self):
180 from agentos.core.middleware import AuditLogMiddleware, MiddlewarePipeline
182 pipe = MiddlewarePipeline([AuditLogMiddleware()])
183 pipe.remove("audit_log")
184 assert "audit_log" not in pipe.middleware_names
186 @pytest.mark.asyncio
187 async def test_phase_filtering(self):
188 from agentos.core.middleware import (
189 MiddlewareContext,
190 MiddlewarePhase,
191 MiddlewarePipeline,
192 PIIMaskingMiddleware,
193 )
195 pipe = MiddlewarePipeline([PIIMaskingMiddleware()])
196 # PIIMaskingMiddleware only listens on PRE_LLM
197 ctx = MiddlewareContext(phase=MiddlewarePhase.PRE_TOOL, tool_name="test")
198 decision = await pipe.pre_tool(ctx)
199 assert decision.allow is True # It should pass through since no middleware listens
202# ══════════════════════════════════════════════════════════════════════════════
203# Enhanced CostTracker + RunCostSession 测试
204# ══════════════════════════════════════════════════════════════════════════════
207class TestRunCostSession:
208 def test_session_lifecycle(self):
209 import time
211 from agentos.cost.tracker import RunCostSession
213 session = RunCostSession(run_id="test-123")
214 assert session.run_id == "test-123"
215 assert session.call_count == 0
216 assert session.total_cost == 0
218 # Record some usage
219 from agentos.cost.tracker import UsageRecord
221 session.records.append(
222 UsageRecord(
223 model="deepseek-v3.1",
224 input_tokens=1000,
225 output_tokens=500,
226 cost_usd=0.01,
227 run_id="test-123",
228 )
229 )
230 session.records.append(
231 UsageRecord(
232 model="deepseek-v3.1",
233 input_tokens=2000,
234 output_tokens=800,
235 cost_usd=0.02,
236 run_id="test-123",
237 )
238 )
239 session.finished_at = time.time()
241 assert session.call_count == 2
242 assert session.total_cost == 0.03
243 assert session.duration_seconds > 0
245 def test_total_tokens(self):
246 from agentos.cost.tracker import RunCostSession, UsageRecord
248 session = RunCostSession(run_id="t")
249 session.records.append(
250 UsageRecord(model="m", input_tokens=100, output_tokens=50, cost_usd=0.0)
251 )
252 session.records.append(
253 UsageRecord(model="m", input_tokens=200, output_tokens=100, cost_usd=0.0)
254 )
255 assert session.total_tokens == {"input": 300, "output": 150, "total": 450}
258class TestCostTrackerEnhanced:
259 def test_start_end_session(self):
260 from agentos.cost.tracker import CostTracker
262 tracker = CostTracker()
263 rid = tracker.start_session()
264 assert len(tracker.active_sessions) == 1
265 session = tracker.end_session(rid)
266 assert session is not None
267 assert session.finished_at is not None
268 assert len(tracker.active_sessions) == 0
270 def test_record_with_session(self):
271 from agentos.cost.tracker import CostTracker
273 tracker = CostTracker()
274 rid = tracker.start_session()
275 tracker.record(
276 "deepseek-v3.1", {"prompt_tokens": 1000, "completion_tokens": 500}, run_id=rid
277 )
278 tracker.record(
279 "deepseek-v3.1", {"prompt_tokens": 500, "completion_tokens": 200}, run_id=rid
280 )
281 session = tracker.end_session(rid)
282 assert session.call_count == 2
283 assert session.total_cost > 0
285 def test_cost_by_session(self):
286 from agentos.cost.tracker import CostTracker
288 tracker = CostTracker()
289 r1 = tracker.start_session()
290 r2 = tracker.start_session()
291 tracker.record(
292 "deepseek-v3.1", {"prompt_tokens": 1000, "completion_tokens": 100}, run_id=r1
293 )
294 tracker.record("deepseek-v3.1", {"prompt_tokens": 500, "completion_tokens": 50}, run_id=r2)
295 costs = tracker.cost_by_session()
296 assert r1 in costs
297 assert r2 in costs
298 assert costs[r2] < costs[r1]
300 def test_get_session_active_and_completed(self):
301 from agentos.cost.tracker import CostTracker
303 tracker = CostTracker()
304 rid = tracker.start_session()
305 assert tracker.get_session(rid) is not None
306 tracker.end_session(rid)
307 assert tracker.get_session(rid) is not None # Should find completed
309 def test_record_with_cache(self):
310 from agentos.cost.tracker import CostTracker
312 tracker = CostTracker()
313 cost = tracker.record_with_cache("deepseek-v3.1", 1000, 500)
314 assert cost > 0
315 assert tracker.total_cost == cost