|
25 | 25 | from uipath_openai_agents.governance.hooks import ( |
26 | 26 | _BEFORE_MODEL_TEXT_CAP, |
27 | 27 | GovernanceAgentHooks, |
| 28 | + _content_text, |
| 29 | + _item_text, |
| 30 | + _latest_input_text, |
| 31 | + _model_response_text, |
| 32 | + _stringify, |
28 | 33 | install_governance, |
29 | 34 | ) |
30 | 35 |
|
@@ -395,6 +400,87 @@ async def test_hooks_return_none(): |
395 | 400 | assert await cb.on_tool_end(None, FakeAgent(), FakeTool("t"), {}) is None # type: ignore[func-returns-value] |
396 | 401 |
|
397 | 402 |
|
| 403 | +# -------------------------------------------------------------------------- |
| 404 | +# coverage: swallow paths on every hook, boundary delegation, extraction edges |
| 405 | +# -------------------------------------------------------------------------- |
| 406 | + |
| 407 | + |
| 408 | +class _Boom: |
| 409 | + """Evaluator whose every evaluate_* raises a non-block error.""" |
| 410 | + |
| 411 | + def __getattr__(self, _name: str) -> Any: |
| 412 | + def _raise(*_a: Any, **_k: Any) -> None: |
| 413 | + raise RuntimeError("evaluator bug") |
| 414 | + |
| 415 | + return _raise |
| 416 | + |
| 417 | + |
| 418 | +@pytest.mark.parametrize( |
| 419 | + "invoke", |
| 420 | + [ |
| 421 | + lambda cb: cb.on_llm_end(None, FakeAgent(), SimpleNamespace(output=[])), |
| 422 | + lambda cb: cb.on_tool_start(None, FakeAgent(), FakeTool("t")), |
| 423 | + lambda cb: cb.on_tool_end(None, FakeAgent(), FakeTool("t"), {"r": 1}), |
| 424 | + ], |
| 425 | +) |
| 426 | +async def test_model_and_tool_hooks_swallow_non_block_errors(invoke, caplog): |
| 427 | + cb = GovernanceAgentHooks(evaluator=_Boom(), agent_name="a", session_id="s") # type: ignore[arg-type] |
| 428 | + with _capture_hooks_logs(caplog): |
| 429 | + await invoke(cb) # must NOT raise — a governance bug can't break the run |
| 430 | + assert any("governance check failed" in r.message for r in caplog.records) |
| 431 | + |
| 432 | + |
| 433 | +class _InnerBoundary: |
| 434 | + def __init__(self) -> None: |
| 435 | + self.seen: List[str] = [] |
| 436 | + |
| 437 | + async def on_start(self, *_a: Any) -> None: |
| 438 | + self.seen.append("on_start") |
| 439 | + |
| 440 | + async def on_end(self, *_a: Any) -> None: |
| 441 | + self.seen.append("on_end") |
| 442 | + |
| 443 | + async def on_handoff(self, *_a: Any) -> None: |
| 444 | + self.seen.append("on_handoff") |
| 445 | + |
| 446 | + |
| 447 | +async def test_boundary_hooks_delegate_to_inner(): |
| 448 | + inner = _InnerBoundary() |
| 449 | + cb = _make_hooks(FakeEvaluator(), inner=inner) |
| 450 | + await cb.on_start(None, FakeAgent()) |
| 451 | + await cb.on_end(None, FakeAgent(), "out") |
| 452 | + await cb.on_handoff(None, FakeAgent(), FakeAgent()) |
| 453 | + assert inner.seen == ["on_start", "on_end", "on_handoff"] |
| 454 | + |
| 455 | + |
| 456 | +async def test_delegate_swallows_inner_hook_error(caplog): |
| 457 | + class _BadInner: |
| 458 | + async def on_llm_start(self, *_a: Any) -> None: |
| 459 | + raise RuntimeError("inner boom") |
| 460 | + |
| 461 | + cb = _make_hooks(FakeEvaluator(), inner=_BadInner()) |
| 462 | + with _capture_hooks_logs(caplog): |
| 463 | + await cb.on_llm_start(None, FakeAgent(), None, [_msg("x")]) # must not raise |
| 464 | + assert any("chained user hook" in r.message for r in caplog.records) |
| 465 | + |
| 466 | + |
| 467 | +def test_extraction_edges(): |
| 468 | + # _stringify: str passthrough; circular ref → str() fallback (not a crash) |
| 469 | + assert _stringify("hi") == "hi" |
| 470 | + circular: dict[str, Any] = {} |
| 471 | + circular["self"] = circular |
| 472 | + assert isinstance(_stringify(circular), str) |
| 473 | + # _item_text: tool-result output-only item |
| 474 | + assert "42" in _item_text({"output": {"balance": 42}}) |
| 475 | + # _content_text: object exposing .text, and a bare-string part in a list |
| 476 | + assert _content_text(SimpleNamespace(text="hello")) == "hello" |
| 477 | + assert "raw" in _content_text(["raw", {"text": "block"}]) |
| 478 | + # _model_response_text: response with no .output → falls back to item text |
| 479 | + assert _model_response_text(SimpleNamespace(content="direct")) == "direct" |
| 480 | + # _latest_input_text: single (non-list) item |
| 481 | + assert _latest_input_text(_msg("solo")) == "solo" |
| 482 | + |
| 483 | + |
398 | 484 | # -------------------------------------------------------------------------- |
399 | 485 | # Factory wiring — the evaluator kwarg drives install_governance |
400 | 486 | # -------------------------------------------------------------------------- |
|
0 commit comments