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:
teknium1
2026-03-04 14:22:30 -08:00
parent b2a9f6beaa
commit 093acd72dd
2 changed files with 66 additions and 2 deletions

View File

@@ -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)

View File

@@ -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."""