fix: catch exceptions from check_fn in is_toolset_available()
get_definitions() already wrapped check_fn() calls in try/except, but is_toolset_available() did not. A failing check (network error, missing import, bad config) would propagate uncaught and crash the CLI banner, agent startup, and tools-info display. Now is_toolset_available() catches all exceptions and returns False, matching the existing pattern in get_definitions(). Added 4 tests covering exception handling in is_toolset_available(), check_toolset_requirements(), get_definitions(), and check_tool_availability(). Closes #402
This commit is contained in:
@@ -119,3 +119,57 @@ class TestToolsetAvailability:
|
||||
result = json.loads(reg.dispatch("bad", {}))
|
||||
assert "error" in result
|
||||
assert "RuntimeError" in result["error"]
|
||||
|
||||
|
||||
class TestCheckFnExceptionHandling:
|
||||
"""Verify that a raising check_fn is caught rather than crashing."""
|
||||
|
||||
def test_is_toolset_available_catches_exception(self):
|
||||
reg = ToolRegistry()
|
||||
reg.register(
|
||||
name="t",
|
||||
toolset="broken",
|
||||
schema=_make_schema(),
|
||||
handler=_dummy_handler,
|
||||
check_fn=lambda: 1 / 0, # ZeroDivisionError
|
||||
)
|
||||
# Should return False, not raise
|
||||
assert reg.is_toolset_available("broken") is False
|
||||
|
||||
def test_check_toolset_requirements_survives_raising_check(self):
|
||||
reg = ToolRegistry()
|
||||
reg.register(name="a", toolset="good", schema=_make_schema(), handler=_dummy_handler, check_fn=lambda: True)
|
||||
reg.register(name="b", toolset="bad", schema=_make_schema(), handler=_dummy_handler, check_fn=lambda: (_ for _ in ()).throw(ImportError("no module")))
|
||||
|
||||
reqs = reg.check_toolset_requirements()
|
||||
assert reqs["good"] is True
|
||||
assert reqs["bad"] is False
|
||||
|
||||
def test_get_definitions_skips_raising_check(self):
|
||||
reg = ToolRegistry()
|
||||
reg.register(
|
||||
name="ok_tool",
|
||||
toolset="s",
|
||||
schema=_make_schema("ok_tool"),
|
||||
handler=_dummy_handler,
|
||||
check_fn=lambda: True,
|
||||
)
|
||||
reg.register(
|
||||
name="bad_tool",
|
||||
toolset="s2",
|
||||
schema=_make_schema("bad_tool"),
|
||||
handler=_dummy_handler,
|
||||
check_fn=lambda: (_ for _ in ()).throw(OSError("network down")),
|
||||
)
|
||||
defs = reg.get_definitions({"ok_tool", "bad_tool"})
|
||||
assert len(defs) == 1
|
||||
assert defs[0]["function"]["name"] == "ok_tool"
|
||||
|
||||
def test_check_tool_availability_survives_raising_check(self):
|
||||
reg = ToolRegistry()
|
||||
reg.register(name="a", toolset="works", schema=_make_schema(), handler=_dummy_handler, check_fn=lambda: True)
|
||||
reg.register(name="b", toolset="crashes", schema=_make_schema(), handler=_dummy_handler, check_fn=lambda: 1 / 0)
|
||||
|
||||
available, unavailable = reg.check_tool_availability()
|
||||
assert "works" in available
|
||||
assert any(u["name"] == "crashes" for u in unavailable)
|
||||
|
||||
@@ -146,9 +146,19 @@ class ToolRegistry:
|
||||
return {name: e.toolset for name, e in self._tools.items()}
|
||||
|
||||
def is_toolset_available(self, toolset: str) -> bool:
|
||||
"""Check if a toolset's requirements are met."""
|
||||
"""Check if a toolset's requirements are met.
|
||||
|
||||
Returns False (rather than crashing) when the check function raises
|
||||
an unexpected exception (e.g. network error, missing import, bad config).
|
||||
"""
|
||||
check = self._toolset_checks.get(toolset)
|
||||
return check() if check else True
|
||||
if not check:
|
||||
return True
|
||||
try:
|
||||
return bool(check())
|
||||
except Exception:
|
||||
logger.debug("Toolset %s check raised; marking unavailable", toolset)
|
||||
return False
|
||||
|
||||
def check_toolset_requirements(self) -> Dict[str, bool]:
|
||||
"""Return ``{toolset: available_bool}`` for every toolset."""
|
||||
|
||||
Reference in New Issue
Block a user