| 61 | |
| 62 | |
| 63 | class CompositeRunHooksTests(unittest.IsolatedAsyncioTestCase): |
| 64 | async def test_fan_out_in_registration_order(self) -> None: |
| 65 | log: list[tuple[str, str]] = [] |
| 66 | a = _RecordingHooks("a", log) |
| 67 | b = _RecordingHooks("b", log) |
| 68 | composite = _CompositeRunHooks([a, b]) |
| 69 | await composite.on_llm_start() |
| 70 | await composite.on_llm_end() |
| 71 | await composite.on_agent_start() |
| 72 | await composite.on_agent_end() |
| 73 | await composite.on_handoff() |
| 74 | await composite.on_tool_start() |
| 75 | await composite.on_tool_end() |
| 76 | # Each method fires for both hooks in registration order. |
| 77 | for method in ( |
| 78 | "on_llm_start", |
| 79 | "on_llm_end", |
| 80 | "on_agent_start", |
| 81 | "on_agent_end", |
| 82 | "on_handoff", |
| 83 | "on_tool_start", |
| 84 | "on_tool_end", |
| 85 | ): |
| 86 | self.assertEqual( |
| 87 | [entry for entry in log if entry[1] == method], |
| 88 | [("a", method), ("b", method)], |
| 89 | ) |
| 90 | |
| 91 | async def test_earlier_hook_exception_short_circuits(self) -> None: |
| 92 | class _Boom(RunHooks[Any]): |
| 93 | async def on_llm_start(self, *_: Any, **__: Any) -> None: |
| 94 | raise RuntimeError("boom") |
| 95 | |
| 96 | log: list[tuple[str, str]] = [] |
| 97 | composite = _CompositeRunHooks([_Boom(), _RecordingHooks("b", log)]) |
| 98 | with self.assertRaises(RuntimeError): |
| 99 | await composite.on_llm_start() |
| 100 | self.assertEqual(log, []) |
| 101 | |
| 102 | |
| 103 | class CollectHooksTests(unittest.TestCase): |
nothing calls this directly
no outgoing calls
no test coverage detected