|
6 | 6 | import pytest |
7 | 7 | from agent_control import evaluation |
8 | 8 | from agent_control.evaluation import EvaluationResult |
| 9 | +from agent_control_models import Step |
9 | 10 | from pydantic import ValidationError |
10 | 11 |
|
11 | 12 |
|
@@ -228,6 +229,85 @@ async def test_evaluate_controls_preserves_explicit_tools_and_ground_truth(monke |
228 | 229 | assert step.ground_truth == {"answer": "expected"} |
229 | 230 |
|
230 | 231 |
|
| 232 | +def test_step_legacy_serialization_is_unchanged_without_children(): |
| 233 | + """A Step without children keeps the existing serialized payload.""" |
| 234 | + step = Step(type="llm", name="chat", input="hello") |
| 235 | + |
| 236 | + assert step.model_dump(mode="json") == { |
| 237 | + "type": "llm", |
| 238 | + "name": "chat", |
| 239 | + "input": "hello", |
| 240 | + "output": None, |
| 241 | + "context": None, |
| 242 | + "tools": None, |
| 243 | + "ground_truth": None, |
| 244 | + } |
| 245 | + assert Step(type="llm", name="chat", input="hello", children=None).children is None |
| 246 | + |
| 247 | + |
| 248 | +def test_step_accepts_and_preserves_recursive_children(): |
| 249 | + """Nested child steps retain all provider-neutral runtime fields.""" |
| 250 | + child_payload = { |
| 251 | + "type": "llm", |
| 252 | + "name": "child", |
| 253 | + "input": {"prompt": "question"}, |
| 254 | + "output": "answer", |
| 255 | + "context": {"conversation": ["earlier"]}, |
| 256 | + "tools": [{"name": "lookup", "input_schema": {"type": "object"}}], |
| 257 | + "ground_truth": {"answer": "expected"}, |
| 258 | + "children": [ |
| 259 | + {"type": "retriever", "name": "search", "input": {"query": "q"}} |
| 260 | + ], |
| 261 | + } |
| 262 | + parent = Step(type="llm", name="parent", input="hello", children=[child_payload]) |
| 263 | + |
| 264 | + assert parent.children is not None |
| 265 | + child = parent.children[0] |
| 266 | + assert child.type == child_payload["type"] |
| 267 | + assert child.name == child_payload["name"] |
| 268 | + assert child.input == child_payload["input"] |
| 269 | + assert child.output == child_payload["output"] |
| 270 | + assert child.context == child_payload["context"] |
| 271 | + assert child.tools == child_payload["tools"] |
| 272 | + assert child.ground_truth == child_payload["ground_truth"] |
| 273 | + assert child.children is not None |
| 274 | + assert child.children[0].input == {"query": "q"} |
| 275 | + |
| 276 | + |
| 277 | +def test_step_rejects_invalid_child_values_clearly(): |
| 278 | + with pytest.raises(ValidationError, match="children"): |
| 279 | + Step(type="llm", name="parent", input="hello", children=["not a step"]) |
| 280 | + |
| 281 | + |
| 282 | +@pytest.mark.asyncio |
| 283 | +async def test_evaluate_controls_passes_children_without_recursively_evaluating(monkeypatch): |
| 284 | + """Children are data on the current Step and are not evaluated separately.""" |
| 285 | + mock_check = AsyncMock(return_value=EvaluationResult(is_safe=True, confidence=1.0)) |
| 286 | + monkeypatch.setattr(evaluation, "check_evaluation_with_local", mock_check) |
| 287 | + children = [ |
| 288 | + Step( |
| 289 | + type="tool", |
| 290 | + name="search", |
| 291 | + input={"query": "nested"}, |
| 292 | + children=[Step(type="llm", name="nested", input="prompt")], |
| 293 | + ) |
| 294 | + ] |
| 295 | + |
| 296 | + with patch("agent_control.state.server_url", "http://localhost:8000"): |
| 297 | + await evaluation.evaluate_controls( |
| 298 | + step_name="parent", |
| 299 | + input="hello", |
| 300 | + children=children, |
| 301 | + stage="pre", |
| 302 | + agent_name="test-bot", |
| 303 | + ) |
| 304 | + |
| 305 | + mock_check.assert_awaited_once() |
| 306 | + passed_step = mock_check.call_args.kwargs["step"] |
| 307 | + assert passed_step.children == children |
| 308 | + assert passed_step.children[0].children == children[0].children |
| 309 | + |
| 310 | + |
231 | 311 | @pytest.mark.asyncio |
232 | 312 | async def test_evaluate_controls_uses_session_api_key_header(monkeypatch): |
233 | 313 | """evaluate_controls should pass init's API-key header into the client.""" |
|
0 commit comments