feat: add inference.sh integration (infsh tool + skill) (#1682)
Add inference.sh CLI (infsh) as a tool integration, giving agents access to 150+ AI apps through a single CLI — image gen (FLUX, Reve, Seedream), video (Veo, Wan, Seedance), LLMs, search (Tavily, Exa), 3D, avatar/lipsync, and more. One API key manages all services. Tools: - infsh: run any infsh CLI command (app list, app run, etc.) - infsh_install: install the CLI if not present Registered as an 'inference' toolset (opt-in, not in core tools). Includes comprehensive skill docs with examples for all app categories. Changes from original PR: - NOT added to _HERMES_CORE_TOOLS (available via --toolsets inference) - Added 12 tests covering tool registration, command execution, error handling, timeout, JSON parsing, and install flow Inspired by PR #1021 by @okaris. Co-authored-by: okaris <okaris@users.noreply.github.com>
This commit is contained in:
114
tests/tools/test_infsh_tool.py
Normal file
114
tests/tools/test_infsh_tool.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Tests for tools/infsh_tool.py — inference.sh CLI integration."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.infsh_tool import (
|
||||
check_infsh_requirements,
|
||||
infsh_tool,
|
||||
infsh_install,
|
||||
)
|
||||
|
||||
|
||||
class TestCheckRequirements:
|
||||
def test_returns_bool(self):
|
||||
result = check_infsh_requirements()
|
||||
assert isinstance(result, bool)
|
||||
|
||||
def test_returns_true_when_infsh_on_path(self, monkeypatch):
|
||||
monkeypatch.setattr("shutil.which", lambda cmd: "/usr/local/bin/infsh" if cmd == "infsh" else None)
|
||||
assert check_infsh_requirements() is True
|
||||
|
||||
def test_returns_false_when_missing(self, monkeypatch):
|
||||
monkeypatch.setattr("shutil.which", lambda cmd: None)
|
||||
assert check_infsh_requirements() is False
|
||||
|
||||
|
||||
class TestInfshTool:
|
||||
def test_not_installed_returns_error(self, monkeypatch):
|
||||
monkeypatch.setattr("tools.infsh_tool.check_infsh_requirements", lambda: False)
|
||||
result = json.loads(infsh_tool("app list"))
|
||||
assert result["success"] is False
|
||||
assert "not installed" in result["error"].lower()
|
||||
|
||||
def test_successful_command(self, monkeypatch):
|
||||
monkeypatch.setattr("tools.infsh_tool.check_infsh_requirements", lambda: True)
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = '{"apps": ["flux", "veo"]}'
|
||||
mock_result.stderr = ""
|
||||
|
||||
with patch("subprocess.run", return_value=mock_result) as mock_run:
|
||||
result = json.loads(infsh_tool("app list --search flux"))
|
||||
assert result["success"] is True
|
||||
mock_run.assert_called_once()
|
||||
call_cmd = mock_run.call_args[0][0]
|
||||
assert "infsh app list --search flux" in call_cmd
|
||||
|
||||
def test_failed_command(self, monkeypatch):
|
||||
monkeypatch.setattr("tools.infsh_tool.check_infsh_requirements", lambda: True)
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 1
|
||||
mock_result.stdout = ""
|
||||
mock_result.stderr = "unknown command"
|
||||
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
result = json.loads(infsh_tool("badcommand"))
|
||||
assert result["success"] is False
|
||||
assert result["exit_code"] == 1
|
||||
|
||||
def test_timeout_handled(self, monkeypatch):
|
||||
monkeypatch.setattr("tools.infsh_tool.check_infsh_requirements", lambda: True)
|
||||
|
||||
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("infsh", 300)):
|
||||
result = json.loads(infsh_tool("app run something", timeout=300))
|
||||
assert result["success"] is False
|
||||
assert "timed out" in result["error"].lower()
|
||||
|
||||
def test_json_output_parsed(self, monkeypatch):
|
||||
monkeypatch.setattr("tools.infsh_tool.check_infsh_requirements", lambda: True)
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = '{"url": "https://example.com/image.png"}'
|
||||
mock_result.stderr = ""
|
||||
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
result = json.loads(infsh_tool("app run flux --json"))
|
||||
assert result["success"] is True
|
||||
assert isinstance(result["output"], dict)
|
||||
assert result["output"]["url"] == "https://example.com/image.png"
|
||||
|
||||
|
||||
class TestInfshInstall:
|
||||
def test_already_installed(self, monkeypatch):
|
||||
monkeypatch.setattr("tools.infsh_tool.check_infsh_requirements", lambda: True)
|
||||
monkeypatch.setattr("tools.infsh_tool._get_infsh_path", lambda: "/usr/local/bin/infsh")
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = "infsh v1.2.3"
|
||||
|
||||
with patch("subprocess.run", return_value=mock_result):
|
||||
result = json.loads(infsh_install())
|
||||
assert result["success"] is True
|
||||
assert result["already_installed"] is True
|
||||
|
||||
|
||||
class TestToolRegistration:
|
||||
def test_tools_registered(self):
|
||||
from tools.registry import registry
|
||||
assert "infsh" in registry._tools
|
||||
assert "infsh_install" in registry._tools
|
||||
|
||||
def test_infsh_in_inference_toolset(self):
|
||||
from toolsets import TOOLSETS
|
||||
assert "inference" in TOOLSETS
|
||||
assert "infsh" in TOOLSETS["inference"]["tools"]
|
||||
assert "infsh_install" in TOOLSETS["inference"]["tools"]
|
||||
|
||||
def test_infsh_not_in_core_tools(self):
|
||||
from toolsets import _HERMES_CORE_TOOLS
|
||||
assert "infsh" not in _HERMES_CORE_TOOLS
|
||||
assert "infsh_install" not in _HERMES_CORE_TOOLS
|
||||
Reference in New Issue
Block a user